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

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.

By the end, you can
  • 01
    Place 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.
  • 02
    Read the build's route tableExplain ○, ◐ and ƒ in the build's own words, and what the Revalidate and Expire columns mean.
  • 03
    Map 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:

The tea shop: a home page, a Teas page and a stock APINext.js
app/api/stock/teas/
app/teas/page.tsx
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>  );}
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 (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 demand

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

The Teas page in two parts. Left, a window labeled static shell, made during next build: Our teas, Green tea $4, Mint tea $3, Lemon tea $3, and the fallback Checking stock…. Right, a shaded card made for each visit: In stock today: 17 bags. An arrow takes it into the fallback's place.During next buildstatic shellOur teasGreen tea: $4Mint tea: $3Lemon tea: $3Checking stock…For each visitIn stock today: 17 bagsstreamed in, made for this visit
/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.
Real-life analogyA bakery's morning

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 Suspense fallback, Checking stock…
The main idea

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).
A grid. Columns: next build, each request, the browser. Rows: SSG makes the whole page during next build. ISR makes the whole page during next build, and again on a request after 1 hour. SSR makes the whole page for each request. CSR makes the page without the stock during the build, and the stock in the browser. Mixed, the half circle, makes the static shell during next build and the stock for each request.next buildeach requestthe browserSSG ○whole pageISR ○ 1hwhole pageremade laterSSR ƒwhole pageCSRpage, no stockthe stockMixed ◐static shellthe stock
Each row is one way to make the Teas page, and each column says when a part of it is made: during 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:

The four names, and Partial Prerendering, in Next.js 16
NameWhen the HTML is madeIn Next.js 16 with Cache ComponentsWhat the build prints
SSG: static site generationOnce, during the build. Every visitor gets the same HTML.A page whose work all finishes during the build.○
ISR: incremental static regenerationDuring 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 renderingOn 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 renderingIn 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 PrerenderingA 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.

Playground: one Teas page, five ways to make it
The tea shop, with this version of app/teas/page.tsxNext.js
app/teas/
app/teas/page.tsx
const 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>  );}
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 (4/4)  Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found└ ○ /teas ○  (Static)  prerendered as static content
Try it yourself
Made at build (SSG)

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

Every version was built with next build from Next.js 16.4.3 and run with next start. The Terminal shows what the build printed, the Network tab what the server sent, and Steps what real Chrome did. Reset goes back to the first version.

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. Menu waits for getMenu(), which line 7 of app/supplier.ts caches. 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?

Playground: one visit to the Teas page, two ways
Who does what◐ /teas, with Suspense
What the visitor seesafter step 1
tea-shop.example/teas

A blank page

React isn't running yet: clicks do nothing.

The page isn't complete yet.

Step 1 of 6
Next.js: next build renders the page

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.

Each step is one thing that happens, in order. The screens are what the recordings of the two versions show: the first part of the HTML, then the whole page. Times aren't to scale. Reset goes back to the first journey's first step.

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.

Three versions of the Teas page, side by side. Circle: the heading, the menu and the cached stock are all plain, made during the build. Half circle: the heading and the menu are plain, the stock is shaded, made for each request. f: all three are shaded, the whole page made for each request. An arrow below runs from left to right: more of the page waits for a request.○ /teasOur teasthe menustock, cached◐ /teasOur teasthe menuthe stockƒ /teasOur teasthe menuthe stockmore of the page waits for a request
Plain cards are made during 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 happens to a component during the build
What the component doesDuring next buildWhere it ends up
Only code: text, props, a list written in the fileIt 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 SuspenseIt 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 itIt 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 = falseIt 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:

Playground: where does the stock go?
The tea shop's Teas page, with this changeNext.js
app/api/stock/teas/
app/teas/page.tsx
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>  );}
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 (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 demand
Try it yourself
Inside Suspense

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

Every version was built with next build from Next.js 16.4.3. Only the Teas page or app/supplier.ts changes, as the marked lines show. Reset goes back to the first version.

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" } in next.config.ts moves it, and devIndicators: false hides 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. Revalidate and Expire head 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-found is 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.
A terminal window titled next build with three route lines. Circle slash: made during the build. f slash api slash stock: made for each request. Half circle slash teas, 1h, 1d: the shell made at build time, the stock for each visit, and the cached menu remade after an hour.next build○ /ƒ /api/stock◐ /teas 1h 1dmade during the buildmade for each requestshell at build, stock per visit
Three lines of the shop's route table. The symbol says when each route is made, and 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.

The three symbols in the shop's build
SymbolThe build's wordsWhat a visitor gets
○ Staticprerendered as static contentThe build prerendered the whole page. It can be revalidated later, and Draft Mode can skip it for an editor's request.
◐ Partial Prerenderprerendered as static HTML with dynamic server-streamed contentThe 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.
ƒ Dynamicserver-rendered on demandThe 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:

The tea shop's pages
PageWhat it showsA good plan
HomeThe shop's name and a short text.Nothing to wait for. It's prerendered as static content: ○.
TeasThe 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 appToday's stock, as JSON.A Route Handler that asks the supplier on each call: ƒ.
CartWhat 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:

Shell, stream, or a stopped build?
  • 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, inside Suspense
  • The stock, from an uncached fetch, with no Suspense around it
  • The stock, cached with cacheLife("minutes")
  • The heading, when one Suspense wraps the whole page
Try it yourself
0 of 7 correct

Sort each part of the Teas page: is it in the static shell, streamed in for each request, or does it stop the build?

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

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 Suspense around it.
  • "A page is either static or dynamic." No. With Cache Components, /teas is both: a static shell, and a stock made for each visit. That's ◐.
  • "Suspense makes 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 1d is 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 = false is 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 build prints 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.

Exercise 1 · Warm-upFind the route made on demand

This is the shop's route table. Which route is server-rendered on demand, for each call?

Starter codeText
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

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

    Exercise 2 · Warm-upWhen was the menu fetched?

    This is app/supplier.ts. The shop was built once, then visited twice. When did the supplier get asked for the menu?

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

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

      Exercise 3 · PracticeCount the teas on the home page

      Change app/page.tsx so it says how many teas are on the menu, using getMenu(). What does the route table print for / then?

      Starter codeTSX
      export default function Home() {
        return (
          <main>
            <h1>Tea shop</h1>
            <p>Three teas, packed by hand.</p>
          </main>
        );
      }
      

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

        Exercise 4 · PracticeFix the stock banner

        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?

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

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

          Exercise 5 · ChallengeCache the menu for days

          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?

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

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

            Exercise 6 · ChallengeBring the menu back into the shell

            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?

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

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

              Check your understanding

              For each question, ask what each part waits for, and whether that work is cached.

              The How Next.js renders pages quiz · 7 questionsScore: first tries count
              1. Question 1 of 7This is app/teas/page.tsx, and getMenu() is cached. Which symbol does next build print for /teas?

                Read the code, then predictTSX
                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>
                  );
                }
                

                Choose an answer to see the explanation.

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

              3. 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 predictTSX
                import { 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.

              4. Question 4 of 7Line 3 of this version says export const instant = false. What does the build print for /teas?

                Read the code, then predictTSX
                import { 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.

              5. Question 5 of 7This is the shop's route table. Which route is server-rendered on demand?

                Read the code, then predictText
                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

                Choose an answer to see the explanation.

              6. Question 6 of 7A Client Component on /teas fetches the stock in an Effect. What does the page's HTML say where the stock goes?

                Choose an answer to see the explanation.

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

              CompleteFrontend Clear concepts. Working examples.