Sequential vs parallel fetching, unstable_cache and cache(), revalidatePath scope, connection() and unstable_noStore() for opting into dynamic rendering, and eliminating waterfall chains.
P-1 — Advanced Data Fetching: Patterns, Caching, and Control
Who this is for: Developers who completed the Foundation phase and can build Next.js applications, but who need to move beyond the basics — eliminating waterfall fetches, using
unstable_cachefor persistent query caching, and understanding exactly which Next.js functions force a page into dynamic rendering. This is where "it works" becomes "it performs."
The Problem With Naive Data Fetching
Foundation-level data fetching — await db.query() in Server Components — is correct. But on a real application with multiple data sources, several failure modes emerge quickly:
Waterfall chains. Component A fetches user data, then renders Component B which fetches preferences, then renders Component C which fetches content. Three sequential round-trips when all three could have run in parallel.
No persistence across requests. React.cache() deduplicates within a single render, but the result is gone the moment the response is sent. Every request rebuilds the same data — a user's profile queried 10,000 times per minute means 10,000 database calls per minute, even when the profile hasn't changed.
Unintentional dynamic rendering. You introduce a cookie check in one small utility function and suddenly a page that was statically generated at build time is now server-rendering on every request. The build output flips from ○ to λ and your CDN cache hit rate collapses.
This module addresses all three.
Sequential vs Parallel — Recognising the Waterfall
The waterfall is the most common performance issue in App Router applications. It happens whenever you await multiple independent fetches sequentially:
These three queries don't depend on each other — getPreferences doesn't need the result of getUser. They should run in parallel:
Spotting the pattern: any time you see multiple await statements at the top level of a Server Component or data function, ask: does the second await need the result of the first? If not, they can be parallelised.
Promise.allSettled for Graceful Degradation
Promise.all rejects if any promise rejects — one failed query takes down the whole fetch. For non-critical data, use Promise.allSettled:
The profile still renders if activity fails to load. Use Promise.allSettled for supplementary data, Promise.all for essential data where partial failure means the page can't render meaningfully.
Suspense as a Parallelism Tool
Beyond the loading skeleton use-case from F-4, Suspense has a second role: it allows independent subtrees to fetch in parallel without blocking each other, regardless of how deep the component tree goes.
Each Suspense boundary is independent — StatsPanel fetching doesn't block ActivityFeed from starting. The fastest one renders first; slow ones stream in later. The page never waits for the slowest query before showing anything.
Compare this to a single page-level await for all three data sources: the page would be blank until the slowest one resolves.
The mental model: each <Suspense> boundary is its own independent loading state. Design your component tree so slow data is isolated in its own boundaries, not mixed with fast data in the same await.
unstable_cache — Persisting Query Results Across Requests
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion