cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

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.

By the end, you can
  • 01
    Spot a waterfallRead the order in which requests start and end, and tell when one waits for another without needing to.
  • 02
    Start independent requests togetherUse Promise.all, sibling components and preloading, so requests that don't need each other start at the same time.
  • 03
    Read 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.

The Tea of the week page, with two awaits in a rowNext.js
app/_data/
app/page.tsx
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>  );}
Recorded from Next.js 16.4.3
$ 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 mint

In 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:

Two halves. On the left, titled two awaits, the bar query 1: tea is followed by the bar query 2: reviews, which starts where the first one ends, one step lower, like a waterfall. On the right, titled Promise.all, the two bars start at the same place, one above the other. An arrow under each half points to later.two awaitsquery 1: teaquery 2: reviewsquery 2 starts when query 1 endslaterPromise.allquery 1: teaquery 2: reviewsboth start before either endslater
Each bar is one query, from the line that says it starts to the line that says it ends. With two 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.
Real-life analogyOrdering at a café

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(), then await 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
The main idea

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:

Playground: in line, or together?
The Tea of the week page, in this versionNext.js
app/_data/
app/page.tsx
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>  );}
Recorded from Next.js 16.4.3
$ 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 mint
Try it yourself
Two awaits

Line 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.

Each version was built with next build from Next.js 16.4.3 and run with next start, which printed the query lines while it answered the visits. The lines' order is what matters; nothing here measures time. Reset goes back to the first version.

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:

Three ways two reads can wait
How the code waitsWhat the terminal printedDo the queries overlap?
await one read, then await the otherQuery 1 starts and ends, then query 2 starts and ends.No. Query 2 starts after query 1 has ended.
Promise.all of both readsQuery 1 starts, query 2 starts, then both end.Yes. Both start before either ends.
The second read needs the first one's answerQuery 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:

Playground: a child, or a sibling?
The Tea of the week page, in this versionNext.js
app/_data/
app/page.tsx
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>  );}
Recorded from Next.js 16.4.3
$ 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 mint
Try it yourself
Child after the parent's await

MintTea 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.

Each version was built with next build from Next.js 16.4.3 and run with next start, which printed the query lines while it answered the visits. The lines' order is what matters; nothing here measures time. Reset goes back to the first version.

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.

Two halves. On the left, a crate labeled MintTea holds the card 1, await getTea, and below it, after an arrow, a smaller crate labeled Reviews with the card 2, getReviews starts. On the right, a crate labeled Home holds two cards side by side, TeaName and Reviews, with the note both reads start together.1. await getTea("mint")2. getReviews startsReviewsMintTeaTeaNameReviewsgetTea startsgetReviews startsboth reads start togetherHome
On the left, 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:

Playground: start the reviews before the check
The Mint tea reviews page, in this versionNext.js
app/_data/
app/page.tsx
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>  );}
Recorded from Next.js 16.4.3
$ 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 mint
Try it yourself
Check, then read

Line 6 waits for the menu check, and line 8 renders Reviews after it. The reviews' query starts only after the check has ended.

Each version was built with next build from Next.js 16.4.3 and run with next start, which printed the query lines while it answered the visits. The lines' order is what matters; nothing here measures time. Reset goes back to the first version.

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:

Playground: one tea, two components
The Tea of the week page, in this versionNext.js
app/_data/
app/page.tsx
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>  );}
Recorded from Next.js 16.4.3
$ 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 mint
Try it yourself
No cache()

TeaName 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.

Both versions were built with next build from Next.js 16.4.3 and run with next start, which answered two visits to / and printed the query lines. Reset goes back to the first version.

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.

Two windows, visit 1 and visit 2. In each, the cards TeaName and TeaPrice both have arrows to one shaded card, cached getTea mint, which has one arrow to a single query card: query 1 in the first visit, query 2 in the second.visit 1TeaNameTeaPricecached getTea("mint")query 1visit 2TeaNameTeaPricecached getTea("mint")query 2
With 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:

Three things that keep a result
WhatWhat it works onHow 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 memoizationfetch 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.

Playground: one address, two fetches
The tea shop's stock page, in this versionNext.js
app/
app/page.tsx
import { 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>  );}
Recorded from Next.js 16.4.3
localhost:3000/Tea shop
Tea shop

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.

Try it yourself
Plain fetch

InStock and StockList both call getStock, which fetches the same address. The supplier received one request for each visit.

Both versions were built with next build from Next.js 16.4.3 and run with next start, which answered two visits to /. The stand-in supplier counted the requests it received; the table under the playground lists them. Reset goes back to the first version.

Each row is one version, with the requests the stand-in supplier recorded for the same two visits:

What the stand-in supplier received during two visits, as recorded
VersionRequests receivedWhen
Plain fetch2 × GET /stockAll while next start answered the visits.
fetch with a signal4 × GET /stockAll 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's cache().
What changed since Next.js 14

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.

Read it in Next.js's own code

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 with Promise.all.
  • Between components, let siblings read their own data. Don't return a component that reads after an await it 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 GET fetch is shared for you.

Sort these pairs of reads: which can start together, and which must wait in line?

Together, or in line?
  • The tea and its reviews, both read with the id mint from 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
Try it yourself
0 of 5 correct

Sort each pair of reads: can both start together, or does the second have to wait for the first?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "async code runs its requests at the same time." No. Each await pauses 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 await started 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.

Exercise 1 · Warm-upRead the line-up

In the recorded terminal for this page, which line comes right before query 2 starts: reviews of mint?

Starter codeTSX
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>
  );
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upStart first, await later

    This version keeps both promises, then awaits them. How many queries have started when the first one ends?

    Starter codeTSX
    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>
      );
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix the waterfall between components

      Change the page so the reviews' read doesn't wait for the tea. After your fix, which line does the terminal print second?

      Starter codeTSX
      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>
        );
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeA line that's real

        Here the reviews need the tea's id. Can Promise.all make the two queries start together?

        Starter codeTSX
        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>
          );
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeStart early without cache()

          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?

          Starter codeTSX
          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>
            );
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeWhy two queries?

            In app/_data/shop.ts, getTea is cache(async (tea: { id: string }) => …). Both components below call it. How many queries does one visit run?

            Starter codeTSX
            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>
              );
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              The parallel and sequential fetching quiz · 7 questionsScore: first tries count
              1. Question 1 of 7A component awaits getTea("mint"), then awaits getReviews("mint"). In what order do the recorded queries start and end?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does Promise.all([getTea("mint"), getReviews("mint")]) change?

                Choose an answer to see the explanation.

              3. 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.

              4. Question 4 of 7The page calls preloadReviews("mint"), then Reviews calls getReviews("mint") again. What keeps the reviews from being read twice?

                Choose an answer to see the explanation.

              5. Question 5 of 7getTea is wrapped in cache(). Two components call getTea("mint"), and the page gets two visits. How many queries run in all?

                Choose an answer to see the explanation.

              6. 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.

              7. 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 with Promise.all.
              • A child returned after its parent's await starts 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 GET fetch is 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.

              CompleteFrontend Clear concepts. Working examples.