Skip to the chapter
←ReactGGroundwork⚛React
React/Advanced/A7
40 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛A7 · Part 3 · Advanced

Rendering strategies

Where the HTML is produced, and when — the decision that shapes everything downstream.

6 min read10 sections2 exercises
40 of 57

In this chapter

  • The four, and what actually differs
  • Client-side rendering
  • Server-side rendering
  • Static generation
  • Incremental regeneration

and 5 more

about 6 min
10On this page
  1. The four, and what actually differs
  2. Client-side rendering
  3. Server-side rendering
  4. Static generation
  5. Incremental regeneration
  6. Hydration, and the two ways it hurts
  7. Hydration mismatches: the usual causes, and the fix for each
  8. Metadata and resources, hoisted
  9. Choosing per route, not per app
  10. The one number to check

The four, and what actually differs

HTML madeByTime to first byteFreshness
CSRIn the browser, after JS loadsthe clientfast, but emptyalways current
SSROn every requestthe serverslowest — waits for dataalways current
SSGAt build timethe buildfastest — a fileas old as the deploy
ISRAt build, then regeneratedbuild + backgroundfastestbounded staleness

Client-side rendering

<div id="root"></div>      // the entire document

The server sends an empty shell; the browser downloads the bundle, runs it, fetches data, and paints. Time to first byte is excellent and time to anything useful is not. Crawlers that do not execute JavaScript see nothing, and the whole experience is hostage to the bundle size.

Right for an app behind a login, where SEO is irrelevant and the first load happens once a day. Wrong for anything with a public landing page.

Server-side rendering

React renders to HTML on each request, and the client hydrates — attaches handlers to markup that already exists. The user sees content before the JavaScript arrives.

The cost is that TTFB now includes your slowest query. A 400ms database call means 400ms before a single byte moves. Streaming with Suspense is the fix: send the shell immediately, stream each boundary as its data resolves.

Static generation

Render at build time, serve files from a CDN. Nothing is faster and nothing is cheaper — there is no server in the request path at all. The constraint is that the content must be knowable at build time, and the cost is that changing one word means a rebuild and a redeploy.

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((p) => ({ slug: p.slug }));   // one page per post
}

Incremental regeneration

export const revalidate = 3600;      // serve cached, refresh at most hourly

A static file with an expiry. The first request after the window serves the stale page and triggers a background regeneration; everyone after gets the new one. You choose how stale is acceptable, and pay nothing for freshness you did not ask for.

On-demand revalidation is the sharper version: a webhook from the CMS regenerates exactly the pages that changed, so the content is fresh in seconds without a rebuild.

Hydration, and the two ways it hurts

The mismatch
<p>{new Date().toLocaleTimeString()}</p>     // ✗ server time ≠ client time
<p>{Math.random()}</p>                       // ✗
<p>{localStorage.getItem("name")}</p>        // ✗ no localStorage on the server

If the client's first render disagrees with the server's HTML, React warns and re-renders that subtree from scratch — losing the benefit of SSR exactly where it happened. The fix is to render the server-safe value first and correct it after mount:

const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return <p>{mounted ? localValue : fallback}</p>;

Browser-only components, since React 19.3

"use client";
import { use } from "react";
import { browser } from "react-dom";

function SavedName() {
  use(browser());                                  // server: stop here. browser: carry on.
  return <p>{localStorage.getItem("name")}</p>;
}

<Suspense fallback={<p>Loading…</p>}>
  <SavedName />
</Suspense>

During server rendering, use(browser()) stops the component and leaves the nearest <Suspense> fallback in the HTML. In the browser it returns undefined and the component renders normally. It replaces the mounted flag above, the typeof window check, and a framework's "disable SSR" option — and because the server never produced a value, there is nothing to mismatch.

  • It needs a <Suspense> boundary above it. Without one, the server render fails.
  • Client Components only. A Server Component always runs on the server, so the question never arises there.
  • Pass it to use. Calling browser() alone does nothing, and throwing it is wrong.

The second cost is simply that hydration is work: React walks the whole tree attaching handlers before the page is interactive. A page can look ready and ignore clicks for a second. Selective hydration and Server Components both exist to shrink that window.

Hydration mismatches: the usual causes, and the fix for each

Hydration reuses the server's HTML instead of rebuilding it, so the client's first render must produce exactly the markup the server sent. When it does not, React logs a mismatch and, in the worst case, throws the server HTML away and renders from scratch. React 19 prints a diff showing which text or attribute differed, so read the message before guessing.

CauseWhy it differsFix
new Date(), Date.now()The server and the browser render at different momentsRender the time in an effect, or pass the value from the server as a prop
Locale or timezone formattingThe server runs in UTC with one locale, the user has anotherFormat with an explicit locale and timeZone
Math.random(), generated idsA different value each renderuseId for ids; move randomness into an effect
typeof window !== "undefined" in renderThe branch is false on the server and true in the browserRead browser-only values in an effect, or with useSyncExternalStore and a server snapshot
localStorage or matchMedia read during renderDoes not exist on the serverThe same: an effect, or a store with getServerSnapshot
Invalid HTML nesting (<div> inside <p>)The browser's parser repairs it before React looksFix the markup
Browser extensions and translatorsThey edit the DOM before hydrationNothing in your code is wrong; suppressHydrationWarning on the affected element if it is noisy
// a value that is legitimately different on the client
function Clock() {
  const [now, setNow] = useState(null);                  // null on the server and on the first client render
  useEffect(() => setNow(new Date()), []);
  return <time>{now ? now.toLocaleTimeString("en-GB") : ""}</time>;
}

// a single text node you accept will differ
<time suppressHydrationWarning>{new Date().toISOString()}</time>

suppressHydrationWarning is an escape hatch, not a fix. It works one level deep, on that element's own text and attributes, and it only silences the warning: the mismatched text stays until something re-renders it. Use it for a genuine, harmless difference such as a timestamp, never to hide a bug. The effect version renders the same thing on the server and the first client pass, then updates — which is the pattern that is always correct.

Metadata and resources, hoisted

function Post({ post }) {
  return (
    <article>
      <title>{post.title}</title>              // hoisted to <head>
      <meta name="description" content={post.summary} />
      <link rel="canonical" href={post.url} />
      ...
    </article>
  );
}

React 19 hoists <title>, <meta> and <link> out of wherever you render them and into the document head, deduplicating as it goes. The component that knows the data declares its own metadata, and libraries like Helmet stop being necessary.

import { preload, preconnect, prefetchDNS } from "react-dom";

preconnect("https://api.example.com");          // warm the connection
preload("/fonts/body.woff2", { as: "font" });   // start the download early

These let a component say what the browser will need before it needs it. The most valuable is usually the font or the LCP image: preloading the hero image can move the largest contentful paint by hundreds of milliseconds, because the browser stops discovering it late in the parse.

Choosing per route, not per app

The real answer in a modern framework is that this is not one decision. A marketing page is static, a blog post is ISR, a dashboard is server-rendered per request, and a settings panel behind auth can be client-rendered. Picking one strategy for an entire application is the mistake.

PageStrategyBecause
Landing, docs, marketingSSGSame for everyone, SEO matters
Blog, product catalogueISRChanges occasionally, must stay fast
Personalised feed, dashboardSSRDifferent per user, must be current
Editor, admin behind a loginCSRNo SEO, heavy interaction

The one number to check

For anything public, measure Interaction to Next Paint and the time between first paint and interactivity. A statically served page that ships a megabyte of JavaScript is not fast — it just looks fast until somebody clicks.

Interview answer, one sentence

"The strategies differ in where and when HTML is produced, and the trade is always freshness against latency: static is fastest and stalest, per-request SSR is current but pays your slowest query in TTFB, and ISR picks a bounded staleness in between. It is a per-route decision, and hydration mismatches are what quietly undo SSR's benefit."

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsPick a strategy per routeadvanced
  • 8 testsDiagnose a hydration mismatch from the snippetadvanced
All 54 for this level →
A7

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. A6Previous · A6The use() hook
  2. A7you are here
  3. Next · A8Effect timingFour hooks that all run 'after render', at four different moments.4 min read

01On this page

10 sections · 0%

  1. 01The four, and what actually differs
  2. 02Client-side rendering
  3. 03Server-side rendering
  4. 04Static generation
  5. 05Incremental regeneration
  6. 06Hydration, and the two ways it hurts
  7. 07Hydration mismatches: the usual causes, and the fix for each
  8. 08Metadata and resources, hoisted
  9. 09Choosing per route, not per app
  10. 10The one number to check

[] chapters · t top