cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

Rendering strategies

Client rendering, server rendering, static generation, revalidation and streaming: what HTML arrives, when the page responds, and how to choose per page.

By the end, you can
  • 01
    Say what HTML arrivesTell what the first HTML holds with client rendering, server rendering, static generation, revalidation and streaming, and when JavaScript runs.
  • 02
    Read a page load's timelineFollow the server, network and browser through one page load, and explain time to first byte, first paint, the main content, and when it responds.
  • 03
    Choose a strategy per pageWeigh speed, freshness, search engines and server cost, and pick a strategy for each page of an app, knowing which frameworks offer which.

Two ways the same shop page can arrive

Every React page you've built in this course rendered in the browser: React ran your components there and put the result on the page. That isn't the only place it can happen. Here is a small shop page. Its result is live; click Add.

import { useState } from "react"; const products = [  { id: "tea", name: "Green tea", price: 3 },  { id: "cake", name: "Carrot cake", price: 4 },]; export default function App() {  const [count, setCount] = useState(0);  return (    <main>      <h1>Shop</h1>      <p>Cart: {count}</p>      <ul>        {products.map((product) => (          <li key={product.id}>            {product.name}: ${product.price} <button onClick={() => setCount(count + 1)}>Add</button>          </li>        ))}      </ul>    </main>  );}
ResultLoading…

Lines 3 to 6 are the products. Line 9 keeps the cart's count in state, and line 17 makes one list item per product, each with a button that adds 1. Nothing new. What this lesson asks is: where and when do these components turn into HTML, and what does the browser get first? With a plain Vite app, the server sends this:

The first HTML with client-side renderingHTML
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Shop</title>
    <script type="module" crossorigin src="/assets/index-B7xQ2k.js"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Line 9 is the whole body: an empty <div id="root">. Line 6 loads the JavaScript. Until that file has downloaded and run, the page is blank. A server can instead run the components itself and send what they show:

The first HTML with server-side renderingHTML
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Shop</title>
    <script type="module" crossorigin src="/assets/index-B7xQ2k.js"></script>
  </head>
  <body>
    <div id="root"><main><h1>Shop</h1><p>Cart: <!-- -->0</p><ul><li>Green tea<!-- -->: $<!-- -->3<!-- --> <button>Add</button></li><li>Carrot cake<!-- -->: $<!-- -->4<!-- --> <button>Add</button></li></ul></main></div>
  </body>
</html>

Line 9 now holds the shop: the heading, the count, and both products, as plain HTML. The browser can show it as soon as it arrives. This is the real output of React's server renderer for the component above. The <!-- --> comments mark where one piece of text ends and the next begins, so React in the browser can match them later. The buttons are there, but they don't work until the same JavaScript has loaded.

Two browser windows at the moment the first HTML arrives. Left, client-side rendering: an empty page, with the note that the root div is empty until the JavaScript runs. Right, server-side rendering: the page already shows Shop, Cart: 0, Green tea: $3 and Carrot cake: $4.client-side renderingbrowser(blank)<div id="root"></div>empty until the JavaScript runsserver-side renderingbrowserShop · Cart: 0Green tea: $3Carrot cake: $4<div id="root"><main>…the products are in the HTML
The moment the first HTML arrives. With client-side rendering it holds an empty <div id="root">, so the page stays blank until the JavaScript runs. With server-side rendering the products are already in the HTML.
Real-life analogyFive ways to get lunch

You can get lunch five ways. A meal kit arrives fast, but you cook before you eat. A restaurant cooks for you while you wait. A sandwich from the shelf is instant, but it was made this morning. A shop that restocks the shelf every hour keeps it fresher. And a restaurant can bring the soup while the main course is still cooking.

In real life: A meal kit: you cook it at home
In React: Client-side rendering: the browser renders the page
In real life: A restaurant that cooks each order
In React: Server-side rendering: the server renders each request
In real life: Sandwiches made in the morning, ready on the shelf
In React: Static generation: HTML rendered at build time
In real life: The shelf restocked every hour with fresh sandwiches
In React: Revalidation: static HTML rebuilt now and then
In real life: A meal served course by course, as each is ready
In React: Streaming: HTML sent in pieces as data arrives
The main idea

A rendering strategy decides where and when your components first turn into HTML: in the browser after the JavaScript loads (client-side rendering), on a server for each request (server-side rendering), once at build time (static generation), at build time and again now and then (revalidation), or on the server in pieces as the data arrives (streaming). The components are the same. What changes is what the first HTML holds, how soon the page shows, how soon it responds, and how much work your servers do.

Client-side rendering: the browser builds the page

With client-side rendering (CSR), the server sends the empty HTML and the JavaScript, and the browser does everything else. Data comes last, because the code that asks for it is in that JavaScript. Here, fetchProducts stands in for a request to a server that takes 1.5 seconds. Press Run again under the result to watch it load again.

A page that renders, then fetches its dataPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react"; function fetchProducts() {  return new Promise((resolve) => {    const products = [      { id: "tea", name: "Green tea", price: 3 },      { id: "cake", name: "Carrot cake", price: 4 },    ];    setTimeout(() => resolve(products), 1500);  });} export default function App() {  const [products, setProducts] = useState(null);   useEffect(() => {    fetchProducts().then(setProducts);  }, []);   if (!products) return <p>Loading products…</p>;  return (    <ul>      {products.map((product) => (        <li key={product.id}>          {product.name}: ${product.price}        </li>      ))}    </ul>  );}
ResultLoading…

Lines 3 to 11 fake a server: a promise that gives the products after 1500 ms. Line 14 starts with no products, so line 20 shows Loading products…. Lines 16 to 18 are an Effect that asks for the data after the first render (Fetching data covers this pattern and its pitfalls). When the answer arrives, setProducts stores it, and lines 21 to 29 show the list.

Here is the whole page load as three lanes, server, network and browser, using the lesson's model of a phone on a fast connection:

Client-side rendering on three lanes. Server: index.html, later load data. Network: empty HTML, JavaScript, later data. Browser: blank, then Loading… at 460 ms, then the products at 1390 ms.servernetworkbrowserindex.htmlempty HTMLblankJavaScriptLoading…load datadataproducts460 msfirst paint1390 msproducts
Client-side rendering, step by step, from left to right in time. The browser gets an empty HTML file, downloads and runs the JavaScript (which shows Loading…), and only then asks for the data. The products appear at 1390 ms in the lesson's model.

Client-side rendering, in short:

  • The first HTML is empty. Something shows at 460 ms, after the JavaScript runs, and the products at 1390 ms, after a second trip for the data.
  • Search engines and link previews see little. Google runs JavaScript, but in a later pass; many other crawlers, and the link previews in chat apps, read only the HTML.
  • It's cheap and simple to host. The server only sends files, which a CDN (a network of servers close to your visitors) can do.
  • After the first load, moving between pages is fast, because the app is already running in the browser (Routing shows how).

Server-side rendering: HTML for every request

With server-side rendering (SSR), a server runs your components for each request and sends the HTML they describe. React does this with react-dom/server. Its simplest function, renderToString, takes JSX and returns HTML as text. It runs in a browser too, so you can watch it here:

What a server sends for a product listPop out in the code editor (opens in a new tab)JSX
import { renderToString } from "react-dom/server"; function ProductList({ products }) {  return (    <ul>      {products.map((product) => (        <li key={product.id}>{`${product.name}: $${product.price}`}</li>      ))}    </ul>  );} const products = [  { id: "tea", name: "Green tea", price: 3 },  { id: "cake", name: "Carrot cake", price: 4 },]; // On a real server, this line runs for every request.const html = renderToString(<ProductList products={products} />); export default function App() {  return <pre style={{ whiteSpace: "pre-wrap" }}>{html}</pre>;}
ResultLoading…

Lines 3 to 11 are an ordinary component. Line 19 calls renderToString with it, and the result shows the text it returns: <ul><li>Green tea: $3</li>…. On a server, that text goes inside the page's <div id="root">, as in the overview. Press Edit & run and add a third product on line 15: the HTML grows.

HTML can't respond to clicks. To make the page interactive, the browser loads the same JavaScript, and React runs the components again and connects them to the HTML that's already there. That step is hydration. Until it's done, the page looks ready but isn't. This demo has no server, so line 8 makes the server's HTML itself; line 12 hydrates it 3 seconds later, the way a slow script would arrive. Click Add to cart right away, then again after the sentence changes:

HTML first, JavaScript 3 seconds laterPop out in the code editor (opens in a new tab)JSX
import { hydrateRoot } from "react-dom/client";import { renderToString } from "react-dom/server";import App from "./App.jsx"; const root = document.getElementById("root"); // 1. The server's HTML arrives: the page shows, but nothing responds.root.innerHTML = renderToString(<App />); // 2. Three seconds later the JavaScript has loaded, and React takes over.setTimeout(() => {  hydrateRoot(root, <App />);}, 3000);
ResultLoading…

Line 8 fills the root with the HTML that App renders: you see the price and the button at once. Line 12, hydrateRoot, runs after 3 seconds and takes over that HTML. Before then, nothing listens for clicks, so they're lost: the count stays at 0. In the App.jsx tab, line 8 runs only in the browser after hydration, so the sentence on line 15 changes from HTML only: clicks do nothing yet. to JavaScript has run: clicks work. The lesson Server rendering and hydration explains hydrateRoot, and what happens when the server's HTML and the browser's render don't match.

Server-side rendering on three lanes. Server: load data, render. Network: full HTML after 930 ms, then JavaScript. Browser: the products at 960 ms, then hydrate, and the buttons respond at 1340 ms.servernetworkbrowserload datarenderfull HTMLproductsJavaScripthydrate960 msproducts1340 msresponds
Server-side rendering, from left to right in time. The server loads the data and renders the HTML before it sends a byte, so the first byte waits 930 ms. The products show at 960 ms, but the buttons only respond at 1340 ms, after the JavaScript has hydrated the page. (Times from the lesson's model.)

Server-side rendering, in short:

  • The first HTML has the content, so the products show at 960 ms in the lesson's model, and search engines read them.
  • The first byte waits for the data. The server loads the products before it sends anything: 930 ms here, and slower data makes it later.
  • The page responds only after hydration, at 1340 ms: the gap you clicked into.
  • Every visit costs server work: loading data and rendering, for every request.

Static generation: HTML built ahead of time

If a page is the same for every visitor, there's no need to render it for each one. With static generation (SSG), you render every page once, when you build the site, and save the HTML as files. React 19 added prerender, in react-dom/static, for this: unlike renderToString, it waits for data that Suspense is waiting on, so the files are complete. Frameworks run a build step like this for you; written by hand it looks like this:

build.jsx: one HTML file per productJavaScript
import { writeFile } from "node:fs/promises";
import { prerender } from "react-dom/static";
import { ProductPage } from "./ProductPage.js";
import { loadProducts } from "./data.js";

const products = await loadProducts();

for (const product of products) {
  const { prelude } = await prerender(<ProductPage product={product} />);
  const html = await new Response(prelude).text();
  await writeFile(`dist/products/${product.id}.html`, html);
}

Line 6 loads the products once, at build time. Lines 8 to 12 render ProductPage for each one: line 9 calls prerender, line 10 reads its result as text, and line 11 writes a file. This is the component it renders:

ProductPage.jsx, rendered at build timeJSX
export function ProductPage({ product }) {
  return (
    <main>
      <h1>{product.name}</h1>
      <p>{`$${product.price}`}</p>
    </main>
  );
}

With the shop's two products, the build writes dist/products/tea.html, holding <main><h1>Green tea</h1><p>$3</p></main>, and dist/products/cake.html. A CDN then sends those files as they are.

Static generation on three lanes. Server: built earlier, at build time, then a CDN sends the file after 50 ms. Network: full HTML, then JavaScript. Browser: the products at 80 ms, then hydrate, and the buttons respond at 460 ms.servernetworkbrowserbuiltCDN filefull HTMLproductsJavaScripthydrateearlierat build time80 msproducts460 msresponds
Static generation, from left to right in time. The HTML was rendered at build time, so a CDN close to the visitor sends a ready file: the products show at 80 ms however slow the data is. Hydration still waits for the JavaScript, until 460 ms. (Times from the lesson's model.)

Static generation, in short:

  • It's the fastest first response: a file from a server near the visitor, with the products in it, at 80 ms, whatever the data costs.
  • It's cheap: no rendering per visit.
  • The data is as old as the build. A new price needs a new build, and a site with a million products takes long to build.
  • It can't differ per visitor. Anything personal, like a signed-in name, is added by the browser after hydration.

Revalidation: static pages that refresh themselves

Revalidation keeps static pages fresh without rebuilding the whole site. Next.js calls it Incremental Static Regeneration (ISR). Here is how a Next.js page asks for it (Next.js 16, checked in October 2026; Next.js's newer Cache Components option sets this up differently). A page says how many seconds its HTML stays fresh:

A product page that revalidates every 60 seconds (Next.js)JSX
// app/products/[id]/page.jsx in a Next.js app
export const revalidate = 60;

export async function generateStaticParams() {
  const products = await db.products.all();
  return products.map((product) => ({ id: product.id }));
}

export default async function ProductPage({ params }) {
  const { id } = await params;
  const product = await db.products.find(id);
  return <h1>{product.name}: ${product.price}</h1>;
}

Line 2 exports revalidate = 60. Lines 4 to 7 list the pages to build, one per product, like the build script above (db stands for your database code). Lines 9 to 13 are the page: an async component that reads one product from the database. Next.js renders each page, saves the HTML, and sends the saved copy. Once the copy is older than 60 seconds, the next visitor still gets it, and a rebuild starts in the background. Visitors after the rebuild get the new HTML. Nobody waits for a rebuild. Try it: open the page, raise the price, then wait and open the page again.

Playground: a price change and a 60-second revalidate
Right now0 s
Saved page$3, 0 s old
Database$3
What happened1 event
  1. 0 s The page is built: it says $3.
Try it yourself
At 0 s

The page is built: it says $3.

This replays a model of time-based revalidation with revalidate = 60 and a rebuild that takes 2 seconds, as Next.js describes it. No real server runs. Reset starts again at 0 s.

A price that changes at 20 s shows up only after the first visit after 60 s, and that visit still sees the old price. If a change must show at once, revalidate from the code that makes the change:

Revalidating when the price changes (Next.js)JavaScript
"use server";
import { revalidatePath } from "next/cache";

export async function updatePrice(id, price) {
  await db.products.update(id, { price });
  revalidatePath(`/products/${id}`);
}

Line 1 makes this a Server Function, which a form can call (Form actions; a later lesson, Server Functions, covers them). Line 5 saves the price, and line 6 tells Next.js that the saved page for this product is out of date, so the next visitor gets a fresh one.

Streaming: the page in pieces

Server-side rendering waits for all the data before it sends a byte. Streaming sends the page in pieces instead: first everything that's ready, with a Suspense fallback where a slow part goes, then that part when its data arrives. React's renderToReadableStream does it. This demo runs it in the browser and lists each piece of HTML as it arrives:

Watching the HTML arrive in piecesPop out in the code editor (opens in a new tab)JSX
import { Suspense, use, useEffect, useState } from "react";import { renderToReadableStream } from "react-dom/server"; const reviews = new Promise((resolve) => {  setTimeout(() => resolve(["Fresh and light", "Lovely with honey"]), 2000);}); function Reviews() {  const list = use(reviews);  return <ul>{list.map((text) => <li key={text}>{text}</li>)}</ul>;} function ProductPage() {  return (    <main>      <h1>Green tea: $3</h1>      <Suspense fallback={<p>Loading reviews…</p>}>        <Reviews />      </Suspense>    </main>  );} export default function App() {  const [pieces, setPieces] = useState([]);   useEffect(() => {    let active = true;    const start = performance.now();    renderToReadableStream(<ProductPage />).then(async (stream) => {      const reader = stream.pipeThrough(new TextDecoderStream()).getReader();      while (true) {        const { done, value } = await reader.read();        if (done || !active) return;        const ms = Math.round(performance.now() - start);        setPieces((list) => [...list, { ms, html: value }]);      }    });    return () => {      active = false;    };  }, []);   return (    <ol>      {pieces.map((piece, index) => (        <li key={index}>          after {piece.ms} ms: <code style={{ wordBreak: "break-all" }}>{piece.html}</code>        </li>      ))}    </ol>  );}
ResultLoading…

Lines 4 to 6 are reviews that take 2 seconds. Line 9 reads them with use, which suspends Reviews until they're ready. Lines 17 to 19 wrap it in Suspense with a fallback. Line 30 starts the stream, and lines 32 to 37 read it piece by piece and add each one, with its time, to the list.

The first piece arrives at once. It holds the heading and Loading reviews…, marked with a <template id="B:0">, plus a small script. About 2 seconds later a second piece arrives: the reviews in a hidden <div id="S:0">, and a script that moves them in place of the fallback. A browser shows the first piece while the server is still loading the reviews. The lesson Streaming server rendering covers how it works and how hydration happens piece by piece.

Server Components, at a glance

So far, every strategy sends the JavaScript for every component, because the browser runs them all again to hydrate. Server Components are components that run only on the server, or at build time. They can read a database directly, and their code is never sent to the browser: only what they rendered is. Components that need state or clicks are marked "use client" and work as before.

A Server Component with one Client Component insideJSX
// ProductPage.jsx: a Server Component. It runs on the server only.
import { db } from "./db.js";
import AddToCart from "./AddToCart.jsx";

export default async function ProductPage({ id }) {
  const product = await db.products.find(id);
  return (
    <main>
      <h1>{product.name}: ${product.price}</h1>
      <AddToCart id={product.id} />
    </main>
  );
}
AddToCart.jsx, a Client ComponentJSX
// AddToCart.jsx: a Client Component. Its code is sent to the browser.
"use client";
import { useState } from "react";

export default function AddToCart({ id }) {
  const [added, setAdded] = useState(false);
  return <button onClick={() => setAdded(true)}>{added ? "Added" : "Add to cart"}</button>;
}

In ProductPage.jsx, line 5 is an async component, and line 6 reads the product straight from the database: no API, no Effect. Line 10 uses AddToCart. In AddToCart.jsx, line 2, "use client", marks it as a Client Component: its code goes to the browser, and it hydrates. ProductPage's code doesn't.

Server Components need a framework that supports them, like Next.js's App Router, and they usually work together with streaming server rendering for the first HTML. In this lesson's model they send 120 kB of JavaScript instead of 200 kB. The lesson Server Components goes through them properly.

Every strategy on one timeline

A few numbers describe how a page load feels. How to read the table: each row is one number, what it measures, and what makes it smaller.

The numbers that describe a page load
NumberWhat it measuresWhat makes it smaller
Time to first byte (TTFB)From asking for the page to the first byte of HTML arriving.Files a CDN already has (SSG, ISR), and streaming, which sends the shell before the data is ready.
First contentful paint (FCP)When the browser first shows anything, like a header or Loading….HTML that arrives with content in it: every strategy except client-side rendering.
Largest contentful paint (LCP)When the main content, here the products, shows. It's one of Google's Core Web Vitals.HTML built ahead of time (SSG, ISR); then server rendering and streaming.
When it respondsWhen a click on Add to cart works. Older tools called it time to interactive (TTI); Google now measures responsiveness with INP.Less JavaScript to download and run, and HTML that arrives early so hydration can start sooner.

The playground runs one product page load with each strategy. Pick a strategy and watch its lanes; the table under the lanes compares all six with the same settings. Then press Data: 2000 ms: server rendering gets 1200 ms slower to the first byte, static generation doesn't move, and streaming still shows its shell at 140 ms. Then try Slow connection with Streaming and RSC: less JavaScript means the page responds sooner.

Playground: one page load, six strategies
Client-side renderingfirst HTML: an empty root

The dashed lines mark the first byte, when the products show, and when Add to cart responds.

Every strategy with these settings, in milliseconds
StrategyFirst byteFirst paintProductsResponds
CSR5046013901390
SSR9309609601340
SSG508080460
ISR508080460
Streaming110140960980
RSC110140960980
Try it yourself
Client-side rendering

The first byte comes after 50 ms, but the HTML is empty. The page shows Loading… at 460 ms, once the JavaScript has run, and the products at 1390 ms, after the browser has fetched the data. They respond as soon as they appear.

A model with round numbers for a phone, not a measurement: what matters is what waits for what. Every strategy uses the same scale. Reset goes back to client-side rendering, 800 ms of data, and a fast connection.

How to read the table: each row is a strategy, what its first HTML holds, how fresh its data is, and what kind of page it suits.

Six strategies side by side
StrategyThe first HTMLData as fresh asGood for
Client-side rendering (CSR)An empty root. The browser renders everything.Each visit: the browser fetches it.Pages behind a login, like dashboards, where search engines don't matter.
Server-side rendering (SSR)The whole page, rendered for this request.Each visit: the server loads it.Pages that differ per visitor or per search, and that search engines must read.
Static generation (SSG)The whole page, rendered at build time.The last build.Pages that are the same for everyone and rarely change: about pages, docs, blog posts.
Static + revalidate (ISR)The whole page, saved and rebuilt now and then.The last rebuild: about the revalidate time.Pages that are the same for everyone but change during the day: product pages, news.
Streaming SSRThe shell first, then each part as its data is ready.Each visit.Server-rendered pages with a slow part, like reviews or recommendations.
Server Components (RSC)Like streaming, and the Server Components send no code.Each visit, or the build.Pages with lots of content to show and few parts to click.

A strategy for each page of a shop

You don't pick one strategy for a whole app. A shop's About page, its product pages, its search and its account pages have different needs, and frameworks let you choose per page (per route). Three questions decide most cases:

  1. Is the page the same for everyone? If not, it can't be built ahead of time.
  2. How often does it change? Rarely: static. During the day: static with revalidation.
  3. Must search engines and link previews read it? If so, send HTML with content: render on the server.

How to read the table: each row is a tool, and the rendering it offers (checked against each project's documentation, October 2026).

Who offers what
ToolRendering it offers
Vite with ReactClient-side rendering. The build makes static files, and the browser renders every page.
Next.js (App Router)Static generation, server rendering, revalidation, streaming with Suspense, and Server Components, chosen per route. react.dev calls it the most complete Server Components setup.
React Router (framework mode)Client-side rendering (SPA mode), server rendering, static pre-rendering of the URLs you list, and streaming with Suspense. Its Server Components support is experimental.
TanStack StartFull-document server rendering and streaming, with server functions. react.dev lists it among the up-and-coming frameworks.
AstroStatic HTML by default, server rendering when you turn it on. React components render to HTML, and only the ones you mark get JavaScript.

React's own docs recommend starting a new app with a framework, and note that the frameworks they recommend can all build static, client-only apps and add server rendering per route later (Setting up a React project). Sort these pages with the three questions:

Which strategy fits this page?
  • An About us page, the same for everyone, edited twice a year
  • A blog post that doesn't change after it's published
  • A product page whose price changes a few times a day, found through search engines
  • A news front page that changes every few minutes, the same for everyone
  • Search results for whatever the visitor typed, which search engines should read
  • A product page that shows each signed-in visitor their own price, and must be found in search
  • An admin dashboard behind a login
  • A drawing tool where everything happens in the browser
Try it yourself
0 of 8 correct

Ask: is it the same for everyone, how often does it change, and must search engines read it? Then sort each page.

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

Common mix-ups

  • "Server-side rendering means less JavaScript." No. The browser still downloads every component to hydrate the page. Server Components are what send less.
  • "Once the page shows, it works." No. A server-rendered page shows before it's hydrated, and clicks before hydration do nothing.
  • "Static means the page can't change." No. The HTML is fixed until the next build or revalidation, but after hydration it's a normal React app with state, clicks and data from the browser.
  • "Revalidate = 60 means visitors see changes within 60 seconds." Not quite. The first visit after 60 seconds still gets the old page and starts the rebuild. Without visitors, nothing rebuilds.
  • "Client-side rendering is outdated." No. For apps behind a login it's simple, cheap, and fast after the first load. It's a choice, not a mistake.
  • "Server Components replace server-side rendering." No. They decide which components run only on the server; streaming server rendering still turns the result into the first HTML.

Practice exercises

Some exercises show their result under the code; the ones that ask you to predict show it when you press Edit & run. Guess first, then check.

Exercise 1 · Warm-upWhat the CSR page shows first

This is the page from the client-side rendering section. What does it show for the first 1.5 seconds?

import { useEffect, useState } from "react";

function fetchProducts() {
  return new Promise((resolve) => {
    const products = [
      { id: "tea", name: "Green tea", price: 3 },
      { id: "cake", name: "Carrot cake", price: 4 },
    ];
    setTimeout(() => resolve(products), 1500);
  });
}

export default function App() {
  const [products, setProducts] = useState(null);

  useEffect(() => {
    fetchProducts().then(setProducts);
  }, []);

  if (!products) return <p>Loading products…</p>;
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name}: ${product.price}
        </li>
      ))}
    </ul>
  );
}

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

    Exercise 2 · Warm-upPredict renderToString

    What string does renderToString return on line 7?

    import { renderToString } from "react-dom/server";
    
    function Price({ amount }) {
      return <strong>{`$${amount}`}</strong>;
    }
    
    const html = renderToString(<Price amount={3} />);
    
    export default function App() {
      return <pre>{html}</pre>;
    }

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

      Exercise 3 · PracticeClick before hydration

      Press Run again under the result. Click Add to cart twice before the sentence changes, then once after. What does the button say?

      import { hydrateRoot } from "react-dom/client";import { renderToString } from "react-dom/server";import App from "./App.jsx"; const root = document.getElementById("root"); // 1. The server's HTML arrives: the page shows, but nothing responds.root.innerHTML = renderToString(<App />); // 2. Three seconds later the JavaScript has loaded, and React takes over.setTimeout(() => {  hydrateRoot(root, <App />);}, 3000);
      ResultLoading…

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

        Exercise 4 · PracticeLet the page stream before the reviews

        Here the whole page arrives as one piece, after 2 seconds. Add a Suspense boundary so the heading arrives first. With your fix, what does the first piece show where the reviews will go?

        import { Suspense, use, useEffect, useState } from "react";import { renderToReadableStream } from "react-dom/server"; const reviews = new Promise((resolve) => {  setTimeout(() => resolve(["Fresh and light", "Lovely with honey"]), 2000);}); function Reviews() {  const list = use(reviews);  return <ul>{list.map((text) => <li key={text}>{text}</li>)}</ul>;} function ProductPage() {  return (    <main>      <h1>Green tea: $3</h1>      <Reviews />    </main>  );} export default function App() {  const [pieces, setPieces] = useState([]);   useEffect(() => {    let active = true;    const start = performance.now();    renderToReadableStream(<ProductPage />).then(async (stream) => {      const reader = stream.pipeThrough(new TextDecoderStream()).getReader();      while (true) {        const { done, value } = await reader.read();        if (done || !active) return;        const ms = Math.round(performance.now() - start);        setPieces((list) => [...list, { ms, html: value }]);      }    });    return () => {      active = false;    };  }, []);   return (    <ol>      {pieces.map((piece, index) => (        <li key={index}>          after {piece.ms} ms: <code style={{ wordBreak: "break-all" }}>{piece.html}</code>        </li>      ))}    </ol>  );}
        ResultLoading…

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

          Exercise 5 · ChallengeWrite chooseStrategy

          chooseStrategy always says CSR. Make it follow the three questions from the practical section. What does it print for the product page?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          function chooseStrategy(page) {
            return "CSR";
          }
          
          const pages = [
            { name: "About us", sameForEveryone: true, changes: "rarely", needsSearch: true },
            { name: "Product page", sameForEveryone: true, changes: "hourly", needsSearch: true },
            { name: "Order history", sameForEveryone: false, changes: "often", needsSearch: false },
            { name: "Search results", sameForEveryone: false, changes: "often", needsSearch: true },
          ];
          
          for (const page of pages) console.log(`${page.name}: ${chooseStrategy(page)}`);

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

            Exercise 6 · ChallengeFollow a revalidation

            This is the revalidation playground's model in plain JavaScript. Without running it, what does it print for the visitor at 66 s? Then press Edit & run.

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const revalidate = 60; // seconds the saved page stays fresh
            const rebuild = 2; // seconds a rebuild takes
            let saved = { price: 3, builtAt: 0 };
            let database = 3;
            let rebuilding = null;
            
            function at(time, what) {
              if (rebuilding && rebuilding.doneAt <= time) {
                saved = { price: rebuilding.price, builtAt: rebuilding.doneAt };
                rebuilding = null;
              }
              if (what === "change") {
                database += 1;
                return;
              }
              const stale = time - saved.builtAt >= revalidate;
              if (stale && !rebuilding) rebuilding = { doneAt: time + rebuild, price: database };
              console.log(`${time} s: $${saved.price}${stale ? " (stale)" : ""}`);
            }
            
            at(10, "change");
            at(50, "visit");
            at(65, "visit");
            at(66, "visit");
            at(70, "visit");

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

              Check your understanding

              For each question, ask where the HTML is made, when, and what the browser has to wait for: the server, the data, or the JavaScript.

              The rendering strategies quiz · 7 questionsScore: first tries count
              1. Question 1 of 7With client-side rendering, what is inside <div id="root"> in the first HTML the browser gets?

                Choose an answer to see the explanation.

              2. Question 2 of 7The products take 2 seconds to load from the database. Which number grows by about 2 seconds with server-side rendering, but not with static generation?

                Choose an answer to see the explanation.

              3. Question 3 of 7What does this server code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { renderToString } from "react-dom/server";
                
                function CartBadge({ count }) {
                  return <span>{`${count} items`}</span>;
                }
                
                console.log(renderToString(<CartBadge count={2} />));

                Choose an answer to see the explanation.

              4. Question 4 of 7A page uses revalidate = 60. It was built at 0 s with $3; the price changed to $4 at 30 s. Visitor A opens it at 70 s, and a rebuild takes 2 s. What does A see?

                Choose an answer to see the explanation.

              5. Question 5 of 7When does streaming server rendering help most?

                Choose an answer to see the explanation.

              6. Question 6 of 7What does a Server Component send to the browser?

                Choose an answer to see the explanation.

              7. Question 7 of 7Why does a server-rendered Add to cart button sometimes do nothing for a moment after it appears?

                Choose an answer to see the explanation.

              Key takeaways

              • Client-side rendering sends an empty root; the browser runs the JavaScript, then fetches the data. Simple and cheap, slow to first content.
              • Server-side rendering sends the page's HTML for each request. The first byte waits for the data, and the page responds only after hydration.
              • Static generation renders at build time, so a CDN sends finished HTML fast; revalidation rebuilds it in the background after a set time.
              • Streaming sends the ready parts first and the slow parts later, with Suspense fallbacks in between.
              • Server Components run only on the server and send their result, not their code, so the browser downloads less JavaScript.
              • Choose per page: same for everyone or not, how often it changes, and whether search engines must read it.

              Remember the one-liner.
              The same components can turn into HTML in the browser, on the server per request, or ahead of time; choose for each page by what it must wait for.

              Coming next: Server rendering and hydration. It shows how renderToString and hydrateRoot work together, and how to fix the page when the server and the browser disagree.

              CompleteFrontend Clear concepts. Working examples.