How Next.js renders pages
See when Next.js makes each part of a page: during the build, for each request, or in the browser. Read ○, ◐ and ƒ, and map CSR, SSR, SSG and ISR onto them.
- 01Place each part of a page in timeTell which components Next.js renders during the build, which it renders for each request, and which run in the browser.
- 02Read the build's route tableExplain ○, ◐ and ƒ in the build's own words, and what the Revalidate and Expire columns mean.
- 03Map the old names onto the new modelSay what CSR, SSR, SSG and ISR mean, and how Cache Components decides static and dynamic one component at a time.
One shop, three kinds of pages
Rendering is running your components to make what the visitor gets: the page's HTML, and the RSC payload, React's description of what the Server Components rendered (How Next.js works showed both). Next.js can render a part of a page at three moments: during next build, before anyone visits; on the server, for each visit; or in the browser, after the page's JavaScript has run. This tea shop uses all three. It's a real project, built with Next.js 16.4.3:
app/teas/page.tsximport { Suspense } from "react";import { getMenu, getStock } from "../supplier"; async function Menu() { const teas = await getMenu(); return ( <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} </li> ))} </ul> );} async function Stock() { const stock = await getStock(); return <p>In stock today: {stock.bags} bags</p>;} export default function TeasPage() { return ( <main> <h1>Our teas</h1> <Menu /> <Suspense fallback={<p>Checking stock…</p>}> <Stock /> </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 (5/5) Finalizing page optimization ... Route (app) Revalidate Expire┌ ○ /├ ○ /_not-found├ ƒ /api/stock└ ◐ /teas 1h 1d ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed contentƒ (Dynamic) server-rendered on demandStart in app/supplier.ts. The shop buys its tea from a supplier with an HTTP API, and these two functions ask it for data. In the recording, the supplier is a small stand-in server on the same computer, and both of its answers take 300 ms. getMenu, on lines 6 to 11, asks for the menu. Line 7, "use cache", tells Next.js to keep its result, and line 8 says for how long: cacheLife("hours"). getStock, on lines 14 to 17, asks for today's stock, and caches nothing.
Now app/teas/page.tsx. Menu, on lines 4 to 15, waits for getMenu() and lists the teas. Stock, on lines 17 to 20, waits for getStock(). The page shows the heading on line 25 and the menu on line 26, and lines 27 to 29 put Stock inside Suspense, with the fallback Checking stock…. The third file, app/api/stock/route.ts, is a Route Handler: line 4 answers GET /api/stock with the stock as JSON.
The Terminal tab shows what next build decided, one symbol per route:
○ /: prerendered as static content. Prerendering is rendering ahead of time, during the build. The home page reads nothing, so all of it was made once.◐ /teas: prerendered as static HTML with dynamic server-streamed content. Its heading and its cached menu were made during the build. Its stock is made for each visit.ƒ /api/stock: server-rendered on demand. Every call to it asks the supplier, while the caller waits.
The recording also kept every request the stand-in supplier received. It was asked for the menu once, during the build, and never during the visits. It was asked for the stock three times, all after the build: once for each visit to /teas, and once for the call to /api/stock. Open the Network tab and pick /teas: near the top of the HTML are the heading, the three teas and Checking stock…. Further down, after the scripts, a hidden part holds In stock today: 17 bags, and React moves it into the fallback's place.
/teas in two parts. The heading and the cached menu were made once, during next build, with the fallback Checking stock…. For each visit, the server makes only the stock, and streams it into the fallback's place.A bakery doesn't make everything when a customer walks in. Bread is baked before the doors open. The cake of the day is baked in the morning and again when it gets old. Only the sandwiches are made to order, and the customer holds a numbered ticket meanwhile, with the bread already on the counter.
- In real life: Bread baked before the shop opens
- In Next.js: Parts prerendered during
next build, like the heading - In real life: The cake of the day, baked in the morning and again in the afternoon
- In Next.js: Cached parts with a lifetime, like the menu with
cacheLife("hours") - In real life: A sandwich made when you order it
- In Next.js: Parts rendered for each request, like the stock
- In real life: A numbered ticket while your sandwich is made
- In Next.js: The
Suspensefallback,Checking stock…
Next.js renders each part of a page as early as it can. Whatever can finish during next build, including cached work, is prerendered into the static shell. Uncached work waits inside Suspense and is rendered for each request. The build prints the result for every route: ○, ◐ or ƒ.
CSR, SSR, SSG and ISR in one picture
You'll meet four short names for ways to make a page. They come from a time when a whole page made one choice, and each name says when its HTML is made:
- SSG, static site generation: the HTML is made once, during the build. Every visitor gets the same file.
- ISR, incremental static regeneration: the HTML is made during the build and kept. Once a set time has passed, the next request still gets the kept page, and starts a new one in the background, without a new build.
- SSR, server-side rendering: the HTML is made on the server, for each request. It can still stream, sending the first part before the rest is ready.
- CSR, client-side rendering: the browser makes the content, after the JavaScript has loaded. A client-rendered React app, like a new Vite app, does it for the whole page (Rendering strategies compares them in React).
next build, for each request, or in the browser after the JavaScript runs. One Next.js page can mix them, like the last row.In Next.js's Pages Router, which Next.js still supports, a page picked one: getStaticProps for SSG, with revalidate for ISR, or getServerSideProps for SSR. The App Router without Cache Components (Next.js 13 to 15, or 16 with it off) decides per route instead: reading a cookie makes the whole route dynamic. With Cache Components on, as in new projects, the names still describe what happens, but the choice is made for each component. Here is how each name maps onto Next.js 16. Each row is one name:
| Name | When the HTML is made | In Next.js 16 with Cache Components | What the build prints |
|---|---|---|---|
| SSG: static site generation | Once, during the build. Every visitor gets the same HTML. | A page whose work all finishes during the build. | ○ |
| ISR: incremental static regeneration | During the build. Once a set time has passed, the next request still gets that page and starts a new one in the background. | A page whose cached parts have a lifetime, like cacheLife("hours"). | ○, with Revalidate and Expire times |
| SSR: server-side rendering | On the server, for each request. It can still stream its parts. | A route Next.js can't prerender, like the Teas page with its uncached stock outside Suspense, or a Route Handler that does request-time work. | ƒ |
| CSR: client-side rendering | In the browser, after the JavaScript has loaded. | A Client Component that loads its data in an Effect. | Nothing of its own: the route keeps the symbol of its server part. |
| Partial Prerendering | A static shell during the build; the dynamic parts are streamed in from the server at request time. | The default with Cache Components: uncached parts wait inside Suspense, like the shop's stock, which is rendered for each visit. | ◐ |
Try them on the tea shop. Each button is one version of app/teas/page.tsx, built and run with real Next.js. Before you click, guess the symbol the build prints for /teas.
app/teas/page.tsxNext.jsapp/teas/page.tsxconst teas = [ { id: "green", name: "Green tea", price: 4 }, { id: "mint", name: "Mint tea", price: 3 }, { id: "lemon", name: "Lemon tea", price: 3 },]; export default function TeasPage() { return ( <main> <h1>Our teas</h1> <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} </li> ))} </ul> </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 (4/4) Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found└ ○ /teas ○ (Static) prerendered as static contentThe teas are written in the file on lines 1 to 5. Nothing waits, so next build makes the whole page once: ○ /teas, prerendered as static content. Every visitor gets that same HTML.
The first two versions are both ○, but the second has two times after it, 1h and 1d: that's ISR, with its Revalidate and Expire times. The third is rendered on the server for each request, ƒ, and because it has no Suspense, its visitors wait for the whole page. The fourth is still ○, because the build made everything the server renders; its HTML says Checking stock…, and only its Steps tab shows the browser asking /api/stock after the page loaded (Fetching in the browser goes further). The last one mixes them: the shell is made during the build, and the stock for each visit.
Prerendering at build time
next build doesn't only bundle your code. After Compiled successfully, the Terminal tab says Generating static pages: Next.js renders every route it can, the way a visitor would get it, and saves the result. For a page, that's its HTML and its RSC payload, which the router uses when a visitor arrives with a link.
A component finishes during the build when it waits for nothing, or only for cached work:
- Only code. The home page, and the heading on line 25 of the Teas page, are text written in the file.
- Cached work.
Menuwaits forgetMenu(), which line 7 ofapp/supplier.tscaches. The build asked the supplier for the menu, waited the 300 ms, and kept both the menu and the HTML made from it.
What the build saves for a page is its static shell: the static content and anything cached, plus a fallback for each Suspense whose content waits for uncached or request-time work. Streaming in Next.js defined it the same way. For /, the shell is the whole page. For /teas, it's everything but the stock.
A prerendered page can also change later. Pick the second version in the playground, Remade hourly (ISR), and look at the route table: ○ /teas, then 1h under Revalidate and 1d under Expire. They come from cacheLife("hours"), the lifetime of the menu inside the page:
- Revalidate, 1h: once an hour has passed, the next visit still gets the saved page at once, and Next.js makes a new one in the background.
- Expire, 1d: if nobody asks for the page for a whole day, the next visit waits for a new one.
That's ISR, without any setting on the page: the lifetime of the cached work inside it decides. The Network tab shows it to caches too. This version's /teas comes with Cache-Control: s-maxage=3600, stale-while-revalidate=82800: a shared cache, like a CDN, may keep it for 3,600 seconds, an hour, and then hand out the old copy for 82,800 more seconds while it gets a new one. Together that's 86,400 seconds: one day, the Expire time. The home page, with nothing cached in it, comes with s-maxage=31536000, a year. Cache lifetimes and tags covers the profiles, and Incremental Static Regeneration the whole story.
Rendering on request
Some work can't be done ahead of time. The stock changes all day, and getStock() caches nothing, so there's no stock to put in the page during the build. That part is rendered on request: next start, the server, runs Stock for each visit. The recording shows it: the supplier got one stock request for each visit to /teas.
How long does the visitor wait for that part? It depends on what's around it. Compare two versions of the Teas page: the shop as it is, with Stock inside Suspense, and the blocking version, with no Suspense and export const instant = false. Step through one visit to each. Before you start, guess: after which step does each visitor first see the menu?
◐ /teas, with Suspenseafter step 1A blank page
React isn't running yet: clicks do nothing.
The page isn't complete yet.
Long before any visit, next build runs TeasPage. Menu waits for the cached getMenu(), so the supplier is asked for the menu once. Stock waits for uncached data, so Next.js keeps its fallback, Checking stock…, and saves the static shell.
With Suspense (◐), the menu is on the screen after step 3, and only the stock follows. In the blocking version (ƒ), the visitor sees nothing until step 4, when the whole page arrives, stock included. In the recordings, the first part of the ◐ page's HTML ends with Checking stock…, while the ƒ page's HTML has the stock in place, with no fallback at all. Both pages answer with Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate: a page with something made for this visit mustn't be kept by shared caches.
A Route Handler like app/api/stock/route.ts works the same way. Its GET waits for uncached data, so it runs for each request: ƒ /api/stock. One that waits for nothing is prerendered like a page, as Route Handlers showed.
Static and dynamic, one component at a time
"Static" means made ahead of time, the same for everyone. "Dynamic" means made for a request. Many frameworks, and Next.js before Cache Components, ask that question about a whole page. Next.js's docs describe the model with Cache Components as a spectrum instead: the line between static and dynamic runs between components, not between routes. The Teas page is static where it can be, and dynamic only around the stock.
next build; shaded cards are made for each request. ○ waits for nothing, ◐ waits only for the stock, and ƒ renders the whole page for each request (its menu still comes from the cache).What decides it isn't speed. The supplier took 300 ms for the menu, and 300 ms for the stock. The menu is in the shell anyway, because its work is cached. During the build, Next.js renders each component and watches what it waits for. When a component waits for uncached work, Next.js can't finish it ahead of time, and needs something to show in its place. Each row is one kind of work a component can do:
| What the component does | During next build | Where it ends up |
|---|---|---|
| Only code: text, props, a list written in the file | It finishes. | In the static shell. |
Waits for cached work, with "use cache" | It finishes, and the result is cached with its lifetime. | In the static shell, until its lifetime says to remake it. |
Waits for uncached work, inside Suspense | It can't finish, so Next.js keeps the fallback. | Streamed in from the server at request time. The shop's stock is rendered for each visit. |
Waits for uncached work, with no Suspense around it | It can't finish, and nothing can stand in for it. | Nowhere: the build stops with an error. |
The same, in a route with export const instant = false | It can't finish. instant = false turns off the check for a static shell, so the build doesn't stop. | The page has no shell to send first, so the route is rendered for each request: ƒ. |
Try it with the stock. Each button changes one thing, and each version was built with real Next.js:
app/teas/page.tsximport { Suspense } from "react";import { getMenu, getStock } from "../supplier"; async function Menu() { const teas = await getMenu(); return ( <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} </li> ))} </ul> );} async function Stock() { const stock = await getStock(); return <p>In stock today: {stock.bags} bags</p>;} export default function TeasPage() { return ( <main> <h1>Our teas</h1> <Menu /> <Suspense fallback={<p>Checking stock…</p>}> <Stock /> </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 (5/5) Finalizing page optimization ... Route (app) Revalidate Expire┌ ○ /├ ○ /_not-found├ ƒ /api/stock└ ◐ /teas 1h 1d ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed contentƒ (Dynamic) server-rendered on demandStock waits for an uncached fetch, inside the Suspense on lines 27 to 29. Next.js puts the fallback in the shell and streams the stock for each visit: ◐ /teas.
No Suspense stops the build: Route "/teas": Next.js encountered uncached or runtime data during prerendering. The message lists three fixes, and the other three buttons are those fixes. [stream] puts the part inside Suspense, which is the first version. [cache] caches the data; here the stock may be a few minutes old, so the whole page is ○ again, with 1m and 1h. [block] is export const instant = false. The docs describe it as allowing a blocking route: it turns off the check that the route has a static shell to send at once. It doesn't ask for request-time rendering, and the docs say it doesn't stop prerendering. The route is ƒ because Stock's uncached work still sits outside any Suspense, so the page has no shell to send first. The docs keep that fix for routes with no useful shell.
One more thing about Suspense: it gives Next.js a fallback to show, but it doesn't make anything dynamic by itself. Next.js's docs say a component that does only synchronous work finishes during prerendering, even inside Suspense. What's inside the boundary decides. Partial Prerendering looks at what makes it into the shell, and Cache Components at the caching side.
The dev indicator and the build output
The dev indicator, while you work
While you write code, you run next dev, not next build. It has no route table to print, so Next.js shows an indicator in the page instead. The indicator only exists in next dev, so it isn't in the recordings, which come from next build and next start. What follows is from Next.js's docs and its source code:
- It's the Next.js logo in a corner of the page: bottom left, unless you move it. Click it, and a menu opens.
- The menu's Route line says whether the page you're on is Static or Dynamic. Other lines show the bundler, Turbopack, and any issues.
devIndicators: { position: "bottom-right" }innext.config.tsmoves it, anddevIndicators: falsehides it. Errors still show when it's hidden.
The indicator has two words, and the build has more to say, like ◐ and the Revalidate times. The docs' own advice, when the indicator says Dynamic and you expected Static, is to run a build and read its output. The two words are on lines 103 to 106 of next-devtools/dev-overlay/menu/panel-router.tsx, in Next.js 16.4.3.
The build output, before you ship
Open the shop's Terminal tab again and read it from the top. ▲ Next.js 16.4.3 (Turbopack) names the version and the bundler, and the next two lines say Cache Components and Partial Prefetching are on, as create-next-app sets them. Then the build compiles, type-checks, and generates the pages it can. Last comes the route table:
Route (app)heads the list of the App Router's routes.RevalidateandExpirehead the two columns of times, filled in for routes whose cached work sets a lifetime, like/teas.- Each line is one route, with its symbol.
/_not-foundis the page Next.js adds for addresses that don't exist. - The legend under the table says what each symbol used in it means, in the build's own words.
1h and 1d are the Revalidate and Expire times of the cached menu on /teas.Each row is one symbol: the build's own words first, then what they mean for a visitor.
| Symbol | The build's words | What a visitor gets |
|---|---|---|
○ Static | prerendered as static content | The build prerendered the whole page. It can be revalidated later, and Draft Mode can skip it for an editor's request. |
◐ Partial Prerender | prerendered as static HTML with dynamic server-streamed content | The static shell goes out at once, and the dynamic parts stream in from the server. In the shop, the stock is rendered for each visit. |
ƒ Dynamic | server-rendered on demand | The server renders the route when a request comes in. |
○ doesn't mean the page can never change: ○ /teas 1h 1d is remade in the background. And a route that lists its addresses with generateStaticParams can show one more symbol, ●, which the build explains as prerendered as static HTML (uses generateStaticParams); generateStaticParams covers it.
Planning the shop's pages
Before you write a page, ask what each part of it shows, and how fresh it has to be. Each row is one page of the tea shop, with a plan for it:
| Page | What it shows | A good plan |
|---|---|---|
| Home | The shop's name and a short text. | Nothing to wait for. It's prerendered as static content: ○. |
| Teas | The menu, and how many bags are in stock today. | Cache the menu for hours, and stream the stock inside Suspense: ◐. |
| Stock API, for the shop's warehouse app | Today's stock, as JSON. | A Route Handler that asks the supplier on each call: ƒ. |
| Cart | What this visitor has chosen. | It depends on the visitor's cookie, so it's read for each request, behind Suspense. The next lesson shows how. |
Then check the plan with next build. Sort each part of the Teas page, using what the recordings above showed:
- The heading
Our teas, written in the page - The menu, from
getMenu()with"use cache" - The fallback
Checking stock… - The stock, from an uncached
fetch, insideSuspense - The stock, from an uncached
fetch, with noSuspensearound it - The stock, cached with
cacheLife("minutes") - The heading, when one
Suspensewraps the whole page
Sort each part of the Teas page: is it in the static shell, streamed in for each request, or does it stop the build?
Common mix-ups
- "Slow data fails the build." No. The menu took as long as the stock and is in the shell, because it's cached. What stops the build is uncached work with no
Suspensearound it. - "A page is either static or dynamic." No. With Cache Components,
/teasis both: a static shell, and a stock made for each visit. That's◐. - "
Suspensemakes a component dynamic." No. It gives Next.js a fallback to show. Whether the content waits for a request depends on what it does. - "
○means the page never changes." No.○ /teas 1h 1dis remade in the background, without a new build. - "A Client Component makes the page dynamic." No. The version that fetches the stock in the browser is
○ /teas. The server renders the component's first state,Checking stock…, during the build. - "
instant = falseis the safe choice." No. In the blocking Teas page, visitors waited for the whole page. Keep the static parts in a shell, and let only the parts that need a request wait. - "The dev indicator is the final word." No. It's a hint while you work.
next buildprints the decision for every route.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
This is the shop's route table. Which route is server-rendered on demand, for each call?
Route (app) Revalidate Expire
┌ ○ /
├ ○ /_not-found
├ ƒ /api/stock
└ ◐ /teas 1h 1d
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
ƒ (Dynamic) server-rendered on demand/api/stock. ƒ means server-rendered on demand: each call runs the Route Handler, which asks the supplier for the stock. /teas is ◐, and / and /_not-found are ○.
This is app/supplier.ts. The shop was built once, then visited twice. When did the supplier get asked for the menu?
import { cacheLife } from "next/cache";
type Tea = { id: string; name: string; price: number };
// The menu changes a few times a year: cache it for hours.
export async function getMenu(): Promise<Tea[]> {
"use cache";
cacheLife("hours");
const response = await fetch(`${process.env.SUPPLIER_API_URL}/menu`);
return response.json();
}
// The stock changes all day: ask for it on every visit.
export async function getStock(): Promise<{ bags: number }> {
const response = await fetch(`${process.env.SUPPLIER_API_URL}/stock`);
return response.json();
}
During the build, once. getMenu is cached, so next build called it while prerendering the Teas page and kept the result. The stand-in supplier received GET /menu only during the build, and never during the two visits.
Change app/page.tsx so it says how many teas are on the menu, using getMenu(). What does the route table print for / then?
export default function Home() {
return (
<main>
<h1>Tea shop</h1>
<p>Three teas, packed by hand.</p>
</main>
);
}
import { getMenu } from "./supplier";
export default async function Home() {
const teas = await getMenu();
return (
<main>
<h1>Tea shop</h1>
<p>{teas.length} teas, packed by hand.</p>
</main>
);
}
○ / with 1h and 1d. Everything the page waits for is cached, so the build finishes it, and the menu's lifetime gives the route its Revalidate and Expire times. The recorded build prints exactly that, and the page says 3 teas, packed by hand.
With this home page, next build stops: Route "/": Next.js encountered uncached or runtime data during prerendering. What goes around <StockBanner /> on line 13 to fix it, keeping the stock fresh?
import { getStock } from "./supplier";
async function StockBanner() {
const stock = await getStock();
return <p>Only {stock.bags} bags left today.</p>;
}
export default function Home() {
return (
<main>
<h1>Tea shop</h1>
<p>Three teas, packed by hand.</p>
<StockBanner />
</main>
);
}
import { Suspense } from "react";
import { getStock } from "./supplier";
async function StockBanner() {
const stock = await getStock();
return <p>Only {stock.bags} bags left today.</p>;
}
export default function Home() {
return (
<main>
<h1>Tea shop</h1>
<p>Three teas, packed by hand.</p>
<Suspense fallback={<p>Checking stock…</p>}>
<StockBanner />
</Suspense>
</main>
);
}
<Suspense>, with a fallback like <p>Checking stock…</p>. getStock() isn't cached, so without a fallback the build stops. With it, the recorded build prints ◐ /: the heading and text are in the shell, and Only 17 bags left today. streams in.
The menu changes only a few times a year. Change getMenu to cache it for days. In the version where the Teas page shows only the menu, which Revalidate and Expire times does the build print for /teas?
import { cacheLife } from "next/cache";
type Tea = { id: string; name: string; price: number };
// The menu changes a few times a year: cache it for hours.
export async function getMenu(): Promise<Tea[]> {
"use cache";
cacheLife("hours");
const response = await fetch(`${process.env.SUPPLIER_API_URL}/menu`);
return response.json();
}
// The stock changes all day: ask for it on every visit.
export async function getStock(): Promise<{ bags: number }> {
const response = await fetch(`${process.env.SUPPLIER_API_URL}/stock`);
return response.json();
}
import { cacheLife } from "next/cache";
type Tea = { id: string; name: string; price: number };
// The menu changes a few times a year: cache it for days.
export async function getMenu(): Promise<Tea[]> {
"use cache";
cacheLife("days");
const response = await fetch(`${process.env.SUPPLIER_API_URL}/menu`);
return response.json();
}
// The stock changes all day: ask for it on every visit.
export async function getStock(): Promise<{ bags: number }> {
const response = await fetch(`${process.env.SUPPLIER_API_URL}/stock`);
return response.json();
}
1d and 1w. With cacheLife("days"), the recorded build of the ISR version prints ○ /teas with 1d under Revalidate and 1w under Expire, and /teas comes with Cache-Control: s-maxage=86400, stale-while-revalidate=518400.
This Teas page puts one Suspense around everything. It builds as ◐ /teas, but visitors see only Loading the teas… at first. Move the boundary. What's in the first part of the HTML after your change?
import { Suspense } from "react";
import { getMenu, getStock } from "../supplier";
async function Menu() {
const teas = await getMenu();
return (
<ul>
{teas.map((tea) => (
<li key={tea.id}>
{tea.name}: ${tea.price}
</li>
))}
</ul>
);
}
async function Stock() {
const stock = await getStock();
return <p>In stock today: {stock.bags} bags</p>;
}
export default function TeasPage() {
return (
<Suspense fallback={<p>Loading the teas…</p>}>
<main>
<h1>Our teas</h1>
<Menu />
<Stock />
</main>
</Suspense>
);
}
import { Suspense } from "react";
import { getMenu, getStock } from "../supplier";
async function Menu() {
const teas = await getMenu();
return (
<ul>
{teas.map((tea) => (
<li key={tea.id}>
{tea.name}: ${tea.price}
</li>
))}
</ul>
);
}
async function Stock() {
const stock = await getStock();
return <p>In stock today: {stock.bags} bags</p>;
}
export default function TeasPage() {
return (
<main>
<h1>Our teas</h1>
<Menu />
<Suspense fallback={<p>Checking stock…</p>}>
<Stock />
</Suspense>
</main>
);
}
With one Suspense around the whole page, the first part of the recorded HTML holds only Loading the teas…, because the heading and menu wait with the stock. With Suspense around <Stock /> alone, it holds the heading, the three teas and Checking stock…. Both builds print ◐ /teas: the symbol is the same, the shell isn't.
Check your understanding
For each question, ask what each part waits for, and whether that work is cached.
Question 1 of 7This is
app/teas/page.tsx, andgetMenu()is cached. Which symbol doesnext buildprint for/teas?Read the code, then predictTSXimport { Suspense } from "react"; import { getMenu, getStock } from "../supplier"; async function Menu() { const teas = await getMenu(); return ( <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} </li> ))} </ul> ); } async function Stock() { const stock = await getStock(); return <p>In stock today: {stock.bags} bags</p>; } export default function TeasPage() { return ( <main> <h1>Our teas</h1> <Menu /> <Suspense fallback={<p>Checking stock…</p>}> <Stock /> </Suspense> </main> ); }Choose an answer to see the explanation.
Question 2 of 7The supplier takes 300 ms to answer for the menu and 300 ms for the stock. Why is only the stock streamed in?
Choose an answer to see the explanation.
Question 3 of 7This page reads the cached menu and nothing else. The build prints
○ /teas 1h 1d. Which old name fits it best?Read the code, then predictTSXimport { getMenu } from "../supplier"; export default async function TeasPage() { const teas = await getMenu(); return ( <main> <h1>Our teas</h1> <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} </li> ))} </ul> </main> ); }Choose an answer to see the explanation.
Question 4 of 7Line 3 of this version says
export const instant = false. What does the build print for/teas?Read the code, then predictTSXimport { getMenu, getStock } from "../supplier"; export const instant = false; async function Menu() { const teas = await getMenu(); return ( <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} </li> ))} </ul> ); } async function Stock() { const stock = await getStock(); return <p>In stock today: {stock.bags} bags</p>; } export default function TeasPage() { return ( <main> <h1>Our teas</h1> <Menu /> <Stock /> </main> ); }Choose an answer to see the explanation.
Question 5 of 7This is the shop's route table. Which route is server-rendered on demand?
Read the code, then predictTextRoute (app) Revalidate Expire ┌ ○ / ├ ○ /_not-found ├ ƒ /api/stock └ ◐ /teas 1h 1d ○ (Static) prerendered as static content ◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content ƒ (Dynamic) server-rendered on demandChoose an answer to see the explanation.
Question 6 of 7A Client Component on
/teasfetches the stock in an Effect. What does the page's HTML say where the stock goes?Choose an answer to see the explanation.
Question 7 of 7Where should you check how a route is made before you ship it?
Choose an answer to see the explanation.
Key takeaways
- Next.js renders a part of a page during
next build, for each request, or in the browser. Prerendering is the default for everything that can finish ahead of time. - Cached work finishes during the build, however long it takes. Uncached work waits inside
Suspense, behind a fallback in the static shell, and is rendered for each request. ○is prerendered as static content,◐is a static shell with dynamic server-streamed content, andƒis server-rendered on demand.- Revalidate and Expire times next to a route come from the lifetimes of its cached work. That's ISR, with no setting on the page.
- SSG, ISR, SSR and CSR are still useful names, but with Cache Components they describe parts of a page, not a choice for the whole page.
- The dev indicator says Static or Dynamic while you work; the build's route table is the decision.
Remember the one-liner.
Cached or code-only parts are made during the build; uncached parts wait in Suspense and are made for each request.
Coming next: Request-time APIs. Cookies, headers, the address's query string and connection() only exist once a request comes in: where to read them, and why each one returns a promise.