Parallel and sequential fetching
Spot request waterfalls in Next.js Server Components and remove them with Promise.all, sibling components, preloading, React cache() and fetch memoization.
- 01Spot a waterfallRead the order in which requests start and end, and tell when one waits for another without needing to.
- 02Start independent requests togetherUse Promise.all, sibling components and preloading, so requests that don't need each other start at the same time.
- 03Read the same data once per requestWrap a data function in React's cache(), and know how Next.js does the same for fetch.
A tea page that waits in line
The tea shop's Tea of the week page shows a tea and its reviews. Both come from the shop's database, read in a Server Component, as Fetching data in Server Components showed, and each read takes a moment. If the page asks for the tea, waits for the answer, and only then asks for the reviews, the two waits add up. Here is that page, as a real Next.js 16.4.3 project: it was built with next build and run with next start, and what happened was recorded.
app/page.tsximport { Suspense } from "react";import { getReviews, getTea } from "./_data/shop"; async function MintTea() { const tea = await getTea("mint"); const reviews = await getReviews("mint"); return ( <section> <h2> {tea.name}: ${tea.price} </h2> <ul> {reviews.map((review) => ( <li key={review}>{review}</li> ))} </ul> </section> );} export default function Home() { return ( <main> <h1>Tea of the week</h1> <Suspense fallback={<p>Loading the tea…</p>}> <MintTea /> </Suspense> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ◐ /└ ○ /_not-found ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local: http://127.0.0.1:3000- Network: http://127.0.0.1:3000✓ Readyquery 1 starts: tea mintquery 1 ends: tea mintquery 2 starts: reviews of mintquery 2 ends: reviews of mintIn app/page.tsx, line 5 waits for getTea("mint"), and line 6 waits for getReviews("mint"). An await pauses MintTea until the promise settles, so line 6 doesn't even run until the tea has arrived. Lines 25 to 27 put MintTea inside a Suspense, so the heading shows while it waits, as Loading UI showed.
Now click app/_data/shop.ts, a stand-in for the database. Line 8 calls connection(), which tells Next.js to run what follows for each request, never during the build. Line 9 counts the queries. Line 11 prints a line when a query starts, and line 13 prints another when it ends. Line 12 waits in between, the same for every query, on purpose: it plays the database.
The Terminal tab shows what next build printed, then what next start printed while it answered one visit to /. Read the four query lines in order. Query 1 starts and ends, and only then does query 2 start. The lines don't measure time; their order is what tells the story. The reviews never needed the tea, and yet they waited for it. Requests that run one after another, each starting only when the one before it has ended, are called a waterfall, after the shape their bars make on a timeline:
awaits, the reviews' bar starts where the tea's bar ends. With Promise.all, both start together. The bars follow the order of the recorded lines; they don't measure time.At a café, you could order a tea, wait at the counter while it's poured, and only then order a cake. Or you could order both at once, and wait for both. The tea takes as long either way, and so does the cake. What changes is whether the baker starts while the barista is still pouring.
- In real life: Ordering a tea, waiting at the counter until it's poured, then ordering a cake
- In Next.js:
await getTea(), thenawait getReviews() - In real life: Ordering both at once, then waiting for both
- In Next.js:
await Promise.all([getTea(), getReviews()]) - In real life: The barista and the baker working at the same time
- In Next.js: Two queries running together
- In real life: A cake chosen to go with whichever tea you were given
- In Next.js: A request that needs the first one's answer, so it has to wait
A request should wait for another only when it needs that request's answer. Start every request as soon as what it needs is known, and wait for the answers afterwards. Each link in a waterfall adds its wait to the time before the content behind it can show.
Start both, then wait for both: Promise.all
A promise starts its work when the function is called, not when something awaits it. So the fix is to call both functions first. Promise.all takes a list of promises and gives back one promise, which settles once all of them have. Pick each version and compare the terminals:
app/page.tsximport { Suspense } from "react";import { getReviews, getTea } from "./_data/shop"; async function MintTea() { const tea = await getTea("mint"); const reviews = await getReviews("mint"); return ( <section> <h2> {tea.name}: ${tea.price} </h2> <ul> {reviews.map((review) => ( <li key={review}>{review}</li> ))} </ul> </section> );} export default function Home() { return ( <main> <h1>Tea of the week</h1> <Suspense fallback={<p>Loading the tea…</p>}> <MintTea /> </Suspense> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ◐ /└ ○ /_not-found ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local: http://127.0.0.1:3000- Network: http://127.0.0.1:3000✓ Readyquery 1 starts: tea mintquery 1 ends: tea mintquery 2 starts: reviews of mintquery 2 ends: reviews of mintLine 5 waits for the tea before line 6 even calls getReviews. The terminal prints query 1 ends: tea mint before query 2 starts: reviews of mint.
In the Promise.all version, line 5 calls getTea("mint") and getReviews("mint") before its one await. Both queries start, and then the component waits for both. The terminal prints query 1 starts, query 2 starts, and only then the two ends. The page is the same in both versions: open the Browser tab to see Mint tea: $3 and both reviews. Sequential vs parallel, in the JavaScript course, shows the same idea without Next.js.
Promise.all isn't the only way to write it. Calling both functions on two lines, keeping their promises, and awaiting them afterwards starts both queries just the same. Promise.all is simply the tidy way to wait for a list.
Two things to know about it. If one of the promises rejects, Promise.all rejects too, and the error goes to the nearest error.tsx, as any error in a Server Component does. When one failed read shouldn't hide the other, Promise.allSettled waits for all of them and says which ones failed. And sometimes the line is real. The docs call that sequential data fetching: the second request needs a value from the first, like the reviews of whichever tea the shop picked this week, which need that tea's id. No Promise.all can start a request whose input doesn't exist yet. Each row is one way the two reads can wait, with what its recorded terminal printed:
| How the code waits | What the terminal printed | Do the queries overlap? |
|---|---|---|
await one read, then await the other | Query 1 starts and ends, then query 2 starts and ends. | No. Query 2 starts after query 1 has ended. |
Promise.all of both reads | Query 1 starts, query 2 starts, then both end. | Yes. Both start before either ends. |
| The second read needs the first one's answer | Query 1 starts and ends, then query 2 starts and ends. | No, and it can't: the second read needs a value from the first. |
When the line is real, keep the first request quick, cache it if it rarely changes (the Rendering and caching stage teaches "use cache"), and show what you already have while the rest streams in.
Waterfalls between components
Waterfalls also hide between components, where there's no second await to spot. In the next version, MintTea waits for the tea and then returns Reviews, a component that reads the reviews itself. Nothing in MintTea waits for the reviews. Pick both versions:
app/page.tsximport { Suspense } from "react";import { getReviews, getTea } from "./_data/shop"; async function Reviews({ id }: { id: string }) { const reviews = await getReviews(id); return ( <ul> {reviews.map((review) => ( <li key={review}>{review}</li> ))} </ul> );} async function MintTea() { const tea = await getTea("mint"); return ( <section> <h2> {tea.name}: ${tea.price} </h2> <Suspense fallback={<p>Loading reviews…</p>}> <Reviews id="mint" /> </Suspense> </section> );} export default function Home() { return ( <main> <h1>Tea of the week</h1> <Suspense fallback={<p>Loading the tea…</p>}> <MintTea /> </Suspense> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ◐ /└ ○ /_not-found ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local: http://127.0.0.1:3000- Network: http://127.0.0.1:3000✓ Readyquery 1 starts: tea mintquery 1 ends: tea mintquery 2 starts: reviews of mintquery 2 ends: reviews of mintMintTea waits on line 16, and only then returns Reviews on line 23, whose read on line 5 starts late. The terminal prints start, end, start, end.
In the first version, line 16 waits for the tea. Reviews, on line 23, is part of what MintTea returns, so React only meets it once line 16 is done. Its read, on line 5, starts then. The terminal prints the same line-up as two awaits in a row: start, end, start, end. Splitting the code into components didn't make the reads overlap.
In the second version, the page itself returns TeaName and Reviews side by side, each inside its own Suspense, on lines 28 to 33. React renders the page, finds both, and calls both. Each one starts its read, and React doesn't wait for one before calling the next. The terminal prints both starts before either end. Open the Network tab: the first part of the HTML holds both fallbacks, Loading the tea… and Loading reviews…, and each is replaced once its own read has ended. The docs say the same holds for layouts and pages: Next.js renders a layout and its page in parallel, so each one starts loading its data as soon as it can.
Reviews is part of what MintTea returns, so React meets it only after MintTea's await. On the right, the page returns TeaName and Reviews side by side, and React starts both.Notice line 23 of the first version: <Reviews id="mint" />. The id comes from the page, not from the tea, so the child didn't have to wait. With <Reviews id={tea.id} />, it would: then the child needs the parent's answer, and the waterfall is real.
Starting requests early
Sometimes a component has to wait for one thing before it can decide what to show. The Mint tea reviews page checks that Mint tea is still on the menu, and shows the reviews only if it is. The check has to end before the page decides. Reading the reviews doesn't. Pick each version, in order:
app/page.tsximport { Suspense } from "react";import { isOnMenu } from "./_data/shop";import Reviews from "./reviews"; async function MintTea() { const onMenu = await isOnMenu("mint"); if (!onMenu) return <p>Not on the menu this week.</p>; return <Reviews id="mint" />;} export default function Home() { return ( <main> <h1>Mint tea reviews</h1> <Suspense fallback={<p>Loading reviews…</p>}> <MintTea /> </Suspense> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ◐ /└ ○ /_not-found ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local: http://127.0.0.1:3000- Network: http://127.0.0.1:3000✓ Readyquery 1 starts: menu has mintquery 1 ends: menu has mintquery 2 starts: reviews of mintquery 2 ends: reviews of mintLine 6 waits for the menu check, and line 8 renders Reviews after it. The reviews' query starts only after the check has ended.
In Check, then read, line 6 waits for isOnMenu("mint"), and line 8 returns Reviews from app/reviews.tsx, which reads the reviews. The terminal shows the menu check end before the reviews' query starts.
In preload, no cache(), app/reviews.tsx gets a small function beside its component, preloadReviews, on lines 3 to 5. It calls getReviews(id) and doesn't wait for it: the void in front of the call says, on purpose, "start this, and ignore what it gives back". Line 6 of the page calls it before the check, so the reviews start first and the check right after, and both run together. But read the terminal to the end: a third query reads the reviews again. When Reviews rendered, line 8 of app/reviews.tsx called getReviews("mint") a second time, and nothing told it that the first call was already on its way.
In preload with cache(), the only change is in app/_data/shop.ts: lines 22 to 24 wrap getReviews in cache(), from React. Now the second call with "mint" gets the promise the first call started. The terminal shows two queries, and both start before either ends. That's the preload pattern from the docs: start the request early without await, call the same function again where the data is used, and make sure that function shares matching calls. The docs suggest keeping the preload function next to the component that uses the data, as here, so you see the two together when you move or delete one.
Preloading has a cost: if the check says no, the reviews were read for nothing. Preload when the check usually says yes, or when the wasted read is cheap. There's also a way with no cache() at all. MintTea can call getReviews("mint") itself before the check, keep the promise, and pass it to Reviews as a prop. A Server Component can await a promise it receives, and there's only one call to share.
One query for many components: React cache()
cache() matters well beyond preloading, because pages read the same data in many places. Here the tea's name and its price are two components, and both call getTea("mint"). The recorder asked for the page twice, as two visits:
app/page.tsximport { Suspense } from "react";import { getTea } from "./_data/shop"; async function TeaName() { const tea = await getTea("mint"); return <h2>{tea.name}</h2>;} async function TeaPrice() { const tea = await getTea("mint"); return <p>${tea.price} for 100 g</p>;} export default function Home() { return ( <main> <h1>Tea of the week</h1> <Suspense fallback={<p>Loading the tea…</p>}> <TeaName /> <TeaPrice /> </Suspense> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ◐ /└ ○ /_not-found ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content $ next start▲ Next.js 16.4.3- Local: http://127.0.0.1:3000- Network: http://127.0.0.1:3000✓ Readyquery 1 starts: tea mintquery 2 starts: tea mintquery 1 ends: tea mintquery 2 ends: tea mintquery 3 starts: tea mintquery 4 starts: tea mintquery 3 ends: tea mintquery 4 ends: tea mintTeaName on line 5 and TeaPrice on line 10 each run a query for the same tea: queries 1 and 2 for the first visit, 3 and 4 for the second.
Without cache(), the terminal shows queries 1 and 2 for the first visit, both reading the same tea, then queries 3 and 4 for the second. With it, lines 18 to 20 of app/_data/shop.ts read export const getTea = cache((id: string) => { … }). cache takes a function and gives back a new one that remembers its results. The first call with "mint" runs the query and keeps the promise it returns. Every later call with "mint" during the same request gets that same promise, so both components wait for one query. The terminal shows one query per visit: query 1 for the first, query 2 for the second.
getTea wrapped in cache(), TeaName and TeaPrice share one query in each visit. The next visit starts with nothing remembered, so it runs a query of its own.The second visit ran a query of its own. react.dev says why: for each new server request, React forgets what every cache() function remembered. One visitor never gets another visitor's answer from it. A few more rules, from react.dev:
- Make it once, at the top of a module, and import it everywhere. Calling
cache()inside a component makes a new function on every render. - Pass values that compare equal. React compares each argument with
Object.is: the same string or number matches, but two objects with the same contents are still two objects. Pass the id, not an object built on the spot. - It works in Server Components. Called outside a component's render, the function runs every time.
- It remembers errors too. If the function throws for an argument, later calls with that argument get the same error, until the request ends.
Don't confuse React's cache() with Next.js's "use cache". Each row is one thing that keeps a result, and for how long:
| What | What it works on | How long it keeps the result |
|---|---|---|
React cache() | Any function you wrap, like a database query. | Until the request ends. The next visit runs the query again. |
| Request memoization | fetch with GET, the same address and the same options. Next.js does it for you. | Until the render of that request ends. |
"use cache" | A function or component you mark with the directive. | Across requests, for as long as its cache lifetime says. |
Request memoization for fetch
fetch gets this without cache(). Here, InStock and StockList both call getStock, which fetches the supplier's stock list. The supplier is a stand-in server that the recorder started on this computer: it answers GET /stock with three teas, and it keeps a list of every request it receives. Its address reaches the app in the environment variable SUPPLIER_API_URL.
app/page.tsximport { Suspense } from "react"; type Stock = { id: string; name: string; stock: number }; async function getStock() { const response = await fetch(`${process.env.SUPPLIER_API_URL}/stock`); return (await response.json()) as Stock[];} async function InStock() { const teas = await getStock(); const count = teas.filter((tea) => tea.stock > 0).length; return <p>{count} teas in stock today</p>;} async function StockList() { const teas = await getStock(); return ( <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: {tea.stock > 0 ? `${tea.stock} left` : "Sold out"} </li> ))} </ul> );} export default function Home() { return ( <main> <h1>Tea shop</h1> <Suspense fallback={<p>Asking the supplier…</p>}> <InStock /> <StockList /> </Suspense> </main> );}2 teas in stock today
- Green tea: 12 left
- Mint tea: Sold out
- Lemon tea: 5 left
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
InStock and StockList both call getStock, which fetches the same address. The supplier received one request for each visit.
Each row is one version, with the requests the stand-in supplier recorded for the same two visits:
| Version | Requests received | When |
|---|---|---|
| Plain fetch | 2 × GET /stock | All while next start answered the visits. |
| fetch with a signal | 4 × GET /stock | All while next start answered the visits. |
Both versions show the same page: 2 teas in stock today and the three teas. With the plain fetch, the supplier received 2 requests for 2 visits: one per visit, though two components fetched. Next.js remembers a fetch with GET, the same address and the same options while it renders one request, and gives the second caller the first one's answer. The docs call this memoization, and older docs called it request memoization. In the second version, line 6 makes an AbortController and line 7 passes its signal to fetch. A signal means the caller controls the request, so Next.js leaves it alone, and the supplier received 4: two per visit. The docs give this as the way to opt out.
The rules, from the docs:
- It only applies to GET requests, with the same address and the same options.
- It lasts one render pass. The next request fetches again, as the second visit did.
- It works in layouts, pages and other Server Components, and in functions like
generateMetadata. It doesn't in Route Handlers, which aren't part of the React tree. - For anything that isn't
fetch, like a database query, use React'scache().
In Next.js 13 and 14, fetch on the server also kept its answers between requests by default, and the docs' caching guide listed request memoization as the first of four caching mechanisms. Next.js 15 stopped caching fetch by default. With Cache Components, keeping a result between requests is the job of "use cache", and the docs describe fetch's own caching options in their guide for projects without Cache Components. Memoization stayed as it was: one render pass, with no setting to turn on. The docs now describe it as memoization, separate from caching, in the fetch reference.
Memoization lives in createDedupeFetch, at lines 43 to 131 of dedupe-fetch.ts in Next.js 16.4.3. It keeps its answers with React's own cache(), and lets any fetch with a signal straight through. The Under the hood stage reads it.
Planning the tea page's requests
Planning a page's requests comes down to one question for each pair of reads: does the second one need the first one's answer? If not, they can start together. Then:
- Inside one component, call every function before the first
await, and wait for them withPromise.all. - Between components, let siblings read their own data. Don't return a component that reads after an
awaitit doesn't need. - When a component must wait anyway, start what doesn't depend on the wait first: preload it, or pass the promise down.
- When many components read the same data, wrap your own data functions in
cache(). A GETfetchis shared for you.
Sort these pairs of reads: which can start together, and which must wait in line?
- The tea and its reviews, both read with the id
mintfrom the page - The reviews of whichever tea
getTeaOfTheWeek()returns - The menu check and the reviews, when the reviews don't depend on the check
- The orders of the user that
getUser()finds from the visitor's cookie - The visitor's basket and the list of teas on sale
Sort each pair of reads: can both start together, or does the second have to wait for the first?
Common mix-ups
- "async code runs its requests at the same time." No. Each
awaitpauses the function. Two in a row made query 2 start after query 1 ended. - "Promise.all makes each request faster." No. Each query waited just as long. They overlapped instead of following each other.
- "Splitting a page into components removes waterfalls." No. A child returned after its parent's
awaitstarted its read late. Siblings started together. - "cache() keeps data for the next visitor." No. React forgets it at the end of each request. The second visit printed a query of its own.
- "Next.js caches every fetch." Not since Next.js 15. Memoization shares a GET within one render; the second visit asked the supplier again.
- "Every waterfall is a mistake." No. When the second request needs the first one's answer, the order is real. Make the first one quick, or cache it.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints. They use the same stand-in database: each query prints when it starts and ends, and waits the same time.
In the recorded terminal for this page, which line comes right before query 2 starts: reviews of mint?
import { Suspense } from "react";
import { getReviews, getTea } from "./_data/shop";
async function MintTea() {
const tea = await getTea("mint");
const reviews = await getReviews("mint");
return (
<section>
<h2>
{tea.name}: ${tea.price}
</h2>
<ul>
{reviews.map((review) => (
<li key={review}>{review}</li>
))}
</ul>
</section>
);
}
export default function Home() {
return (
<main>
<h1>Tea of the week</h1>
<Suspense fallback={<p>Loading the tea…</p>}>
<MintTea />
</Suspense>
</main>
);
}
query 1 ends: tea mint. Line 5's await holds MintTea until the tea's query ends, so the reviews' query starts right after that line.
This version keeps both promises, then awaits them. How many queries have started when the first one ends?
import { Suspense } from "react";
import { getReviews, getTea } from "./_data/shop";
async function MintTea() {
const teaPromise = getTea("mint");
const reviewsPromise = getReviews("mint");
const tea = await teaPromise;
const reviews = await reviewsPromise;
return (
<section>
<h2>
{tea.name}: ${tea.price}
</h2>
<ul>
{reviews.map((review) => (
<li key={review}>{review}</li>
))}
</ul>
</section>
);
}
export default function Home() {
return (
<main>
<h1>Tea of the week</h1>
<Suspense fallback={<p>Loading the tea…</p>}>
<MintTea />
</Suspense>
</main>
);
}
2. Lines 5 and 6 start both queries before line 7 waits. The recorded terminal printed query 1 starts, query 2 starts, and then the two ends, the same order as Promise.all.
Change the page so the reviews' read doesn't wait for the tea. After your fix, which line does the terminal print second?
import { Suspense } from "react";
import { getReviews, getTea } from "./_data/shop";
async function Reviews({ id }: { id: string }) {
const reviews = await getReviews(id);
return (
<ul>
{reviews.map((review) => (
<li key={review}>{review}</li>
))}
</ul>
);
}
async function MintTea() {
const tea = await getTea("mint");
return (
<section>
<h2>
{tea.name}: ${tea.price}
</h2>
<Suspense fallback={<p>Loading reviews…</p>}>
<Reviews id="mint" />
</Suspense>
</section>
);
}
export default function Home() {
return (
<main>
<h1>Tea of the week</h1>
<Suspense fallback={<p>Loading the tea…</p>}>
<MintTea />
</Suspense>
</main>
);
}
import { Suspense } from "react";
import { getReviews, getTea } from "./_data/shop";
async function Reviews({ id }: { id: string }) {
const reviews = await getReviews(id);
return (
<ul>
{reviews.map((review) => (
<li key={review}>{review}</li>
))}
</ul>
);
}
async function TeaName({ id }: { id: string }) {
const tea = await getTea(id);
return (
<h2>
{tea.name}: ${tea.price}
</h2>
);
}
export default function Home() {
return (
<main>
<h1>Tea of the week</h1>
<Suspense fallback={<p>Loading the tea…</p>}>
<TeaName id="mint" />
</Suspense>
<Suspense fallback={<p>Loading reviews…</p>}>
<Reviews id="mint" />
</Suspense>
</main>
);
}
query 2 starts: reviews of mint. With TeaName and Reviews as siblings, React calls both, and the reviews' query starts right after the tea's. The recorded terminal printed both starts before either end.
Here the reviews need the tea's id. Can Promise.all make the two queries start together?
import { Suspense } from "react";
import { getReviews, getTea } from "./_data/shop";
async function Reviews({ id }: { id: string }) {
const reviews = await getReviews(id);
return (
<ul>
{reviews.map((review) => (
<li key={review}>{review}</li>
))}
</ul>
);
}
async function MintTea() {
const tea = await getTea("mint");
return (
<section>
<h2>
{tea.name}: ${tea.price}
</h2>
<Suspense fallback={<p>Loading reviews…</p>}>
<Reviews id={tea.id} />
</Suspense>
</section>
);
}
export default function Home() {
return (
<main>
<h1>Tea of the week</h1>
<Suspense fallback={<p>Loading the tea…</p>}>
<MintTea />
</Suspense>
</main>
);
}
No. Line 23 passes tea.id, which only exists once the tea's query has ended. The recorded terminal printed start, end, start, end. Keep the first read quick or cached, and let the reviews stream in after it.
getReviews here has no cache(). Start the reviews before the menu check without calling getReviews twice. How many queries for the reviews does your version run?
import { Suspense } from "react";
import { isOnMenu } from "./_data/shop";
import Reviews from "./reviews";
async function MintTea() {
const onMenu = await isOnMenu("mint");
if (!onMenu) return <p>Not on the menu this week.</p>;
return <Reviews id="mint" />;
}
export default function Home() {
return (
<main>
<h1>Mint tea reviews</h1>
<Suspense fallback={<p>Loading reviews…</p>}>
<MintTea />
</Suspense>
</main>
);
}
import { Suspense } from "react";
import { getReviews, isOnMenu } from "./_data/shop";
import Reviews from "./reviews";
async function MintTea() {
const reviews = getReviews("mint");
const onMenu = await isOnMenu("mint");
if (!onMenu) return <p>Not on the menu this week.</p>;
return <Reviews reviews={reviews} />;
}
export default function Home() {
return (
<main>
<h1>Mint tea reviews</h1>
<Suspense fallback={<p>Loading reviews…</p>}>
<MintTea />
</Suspense>
</main>
);
}
1. Line 6 starts the reviews before the check on line 7, and line 9 passes the promise to Reviews, which awaits it (const list = await reviews in app/reviews.tsx). There's only one call, so nothing needs sharing. The recorded terminal printed two queries: the reviews and the menu check, both started before either ended.
In app/_data/shop.ts, getTea is cache(async (tea: { id: string }) => …). Both components below call it. How many queries does one visit run?
import { Suspense } from "react";
import { getTea } from "./_data/shop";
async function TeaName() {
const tea = await getTea({ id: "mint" });
return <h2>{tea.name}</h2>;
}
async function TeaPrice() {
const tea = await getTea({ id: "mint" });
return <p>${tea.price} for 100 g</p>;
}
export default function Home() {
return (
<main>
<h1>Tea of the week</h1>
<Suspense fallback={<p>Loading the tea…</p>}>
<TeaName />
<TeaPrice />
</Suspense>
</main>
);
}
import { Suspense } from "react";
import { getTea } from "./_data/shop";
async function TeaName() {
const tea = await getTea("mint");
return <h2>{tea.name}</h2>;
}
async function TeaPrice() {
const tea = await getTea("mint");
return <p>${tea.price} for 100 g</p>;
}
export default function Home() {
return (
<main>
<h1>Tea of the week</h1>
<Suspense fallback={<p>Loading the tea…</p>}>
<TeaName />
<TeaPrice />
</Suspense>
</main>
);
}
2. Each { id: "mint" } is a new object, so cache() sees two different arguments and runs two queries; the recorded terminal printed query 1 and query 2. Pass the string instead, getTea("mint"), with getTea taking an id, and one visit runs one query.
Check your understanding
Question 1 of 7A component awaits
getTea("mint"), then awaitsgetReviews("mint"). In what order do the recorded queries start and end?Choose an answer to see the explanation.
Question 2 of 7What does
Promise.all([getTea("mint"), getReviews("mint")])change?Choose an answer to see the explanation.
Question 3 of 7A parent component awaits the tea, then returns a child that reads the reviews. When does the child's query start?
Choose an answer to see the explanation.
Question 4 of 7The page calls
preloadReviews("mint"), thenReviewscallsgetReviews("mint")again. What keeps the reviews from being read twice?Choose an answer to see the explanation.
Question 5 of 7
getTeais wrapped incache(). Two components callgetTea("mint"), and the page gets two visits. How many queries run in all?Choose an answer to see the explanation.
Question 6 of 7Two Server Components fetch the same supplier address with a plain GET while one page renders. How many requests does the supplier receive?
Choose an answer to see the explanation.
Question 7 of 7Which of these keeps a result for later visitors?
Choose an answer to see the explanation.
Key takeaways
- A waterfall is requests that run one after another. Each link adds its wait before the content behind it can show.
- A request starts when its function is called. Call every independent read before the first
await, and wait withPromise.all. - A child returned after its parent's
awaitstarts late. Siblings start together, and the docs say a layout and its page do too. - When a component must wait first, preload what doesn't depend on the wait, or pass its promise down.
- React's
cache()shares one call per argument for the rest of a request. A GETfetchis memoized the same way by Next.js. Neither keeps anything for the next visit;"use cache"does. - When the second request needs the first one's answer, the order is real. Make the first one quick, or cache it.
Remember the one-liner.
Start every request as soon as you know what it needs, and read each piece of data once per request.
Coming next: Streaming with Suspense. It follows the page's HTML as it arrives in pieces, and shows where to put each Suspense.