Watch one web page load three ways at once

Same product page, same mid-range phone, same 4G connection. Only the rendering strategy changes. Then take the controls and test the other four.

CSRBlank until the JavaScript has run
shop.example/alta-trail-shell
Blank0 ms
Sees the product
2.9 s
Can tap the button
2.9 s
SSRBlank until the server has finished
shop.example/alta-trail-shell
Blank0 ms
Sees the product
1.6 s
Can tap the button
4.6 s
PPRStatic shell now, the rest streams in
shop.example/alta-trail-shell
Blank0 ms
Sees the product
289 ms
Can tap the button
1.6 s
0 ms
1The lab

Same page, seven ways to build it

Pick a strategy, then change the phone, the network or the page. The waterfall redraws, the mock browser repaints, and you can drag the playhead to any moment.

Strategy

Server-side rendering, waiting for everything. Render fresh HTML on the server for every request. How it works

shop.example/alta-trail-shell
BlankTry tapping the button
First byteTTFB
1.5 s
First paintFCP
1.6 s
Main contentLCP
1.6 s
InteractiveTTI
4.6 s
Dead zoneLooks ready, ignores taps
3.0 s
JavaScriptShipped and run
320 KB
  • Nothing leaves the server for 1.5 s. It holds the response until the slowest data (1.2 s) is ready and the page is rendered.
  • Every page view costs one server render.
  • Dead zone: the main content is visible at 1.6 s but ignores taps until 4.6 s. That is 3.0 s spent hydrating 320 KB on a 4× slower CPU.
0 ms
NetworkBytes on the wire
ServerWork on the origin
Main threadThe device’s CPU
Connect
Waiting for the server
index.html
JavaScript
Request JS
Fetch data and render HTML
Parse HTML
First paint
Parse the rest
Run JavaScript
Hydrate
  • Connect
  • Waiting
  • HTML
  • JavaScript
  • Data
  • Render
  • Parse
  • Paint
  • Run JS
  • Hydrate

Rendered on the origin for every request. Nothing is sent until the slowest data is back.

How the numbers are made

This is a timing model, not a browser. It is built from a handful of rules, and it is meant to move the way real measurements move. Use it to see why one strategy beats another, and run Lighthouse or WebPageTest for a verdict on your own site.

  • Network. A cold connection costs two round trips. A request costs one more. Transfer time is size × 8 ÷ bandwidth.
  • Device. Running JavaScript costs 1.4 ms per gzipped KB on a fast desktop. Hydration costs 0.8 ms per KB. Both multiply by the CPU slowdown you pick.
  • Server. A render takes 20 ms plus 0.4 ms per KB of HTML. Data fetches run in parallel, so the slowest one decides when blocking SSR can answer.
  • First paint. The browser paints after the first ~14 KB of HTML, the size of the first TCP congestion window.
  • Server Components. The runtime is 45 KB and always ships. The rest of your bundle shrinks to the share you keep on the client, and the flight payload adds 25% to the streamed HTML.
  • Interactive. The end of hydration, or the moment the main content arrives, whichever is later.

The rules live in src/engine/simulate.ts. Hundreds of tests check invariants such as “a slower network never makes anything faster”.

2How each one works

The same request, with the work in a different place

Step through what actually happens between a tap on a link and a usable page. Watch which box does the work, and when the visitor gets to see anything.

Client-side rendering

Ship an empty page and let the browser build it.

The server sends a near-empty HTML file and a JavaScript bundle. The browser runs the bundle, calls your API, then renders. Nothing meaningful is on screen until all three have happened, one after the other.

BrowserThe visitor’s deviceCDNNearest edge nodeServerYour originDataDatabase or API
Step 1 of 6

Reach for it when

  • Authenticated dashboards and admin tools
  • Highly interactive apps: editors, canvases, maps
  • Prototypes and internal tools on plain static hosting

Look elsewhere when

  • Public pages that need to rank or unfurl in chat apps
  • Landing pages where first paint decides conversion
  • Low-end devices on slow networks

Watch out for

  • Three sequential waits: HTML, then JS, then data. Preload the data call to break the chain.
  • Crawlers that do not run JavaScript see an empty page, and link previews are blank.
  • Bundles only grow. Without route splitting, the settings page costs every visitor the whole app.

In code

app/product/[id]/page.tsx
'use client';
import { use, useEffect, useState } from 'react';

export default function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = use(params);
  const [product, setProduct] = useState<Product | null>(null);

  useEffect(() => {
    fetch('/api/products/' + id)
      .then((res) => res.json())
      .then(setProduct);
  }, [id]);

  if (!product) return <ProductSkeleton />;
  return <ProductView product={product} />;
}

Everything runs in the browser. The HTML that reaches the visitor has no product in it.

In an interview

“Pick it when the page is behind a login and SEO does not matter, then keep the bundle small and fetch in parallel.”

Side by side

Click a column to sort by it. Hover a row to follow it across.

Strategy
StrongWeakFairStrongStrongWeakStrongStrong
StrongStrongFairWeakWeakStrongStrongStrong
StrongStrongFairGoodWeakStrongStrongFair
WeakFairFairStrongStrongStrongWeakFair
GoodGoodFairStrongStrongStrongWeakFair
GoodGoodGoodStrongStrongStrongFairFair
StrongStrongGoodGoodGoodStrongGoodWeak

Four filled bars is the best case for that column.

Glossary
TTFB
Time to first byte. How long until the first piece of the response arrives. It includes any server work done before sending.
FCP
First contentful paint. The first moment anything real is drawn: text, an image, a skeleton.
LCP
Largest contentful paint. When the biggest element in the viewport is drawn. Usually the hero image or headline.
TTI
Time to interactive. The point where the main content responds to taps. This site measures it as the end of hydration.
INP
Interaction to next paint. How long the page takes to react to a tap. Hydration delays the first tap.
Hydration
React re-running your components in the browser to attach handlers to HTML that the server already produced.
Flight payload
The compact stream that describes the output of Server Components, including references to client components.
Stale-while-revalidate
Serve the old copy immediately, refresh it in the background, and let the next visitor have the new one.
Suspense boundary
A place where React can show a fallback while a part of the tree waits for data.
Dead zone
The time between a page looking ready and actually responding. This lab calls it out because it is the cost of SSR and SSG that is easiest to miss.
3The cache clock

Why “revalidate: 60” does not mean fresh every 60 seconds

Run an ISR cache in fast-forward. Publish in the CMS, send visitors, and watch who sees which version, and who triggers the rebuild.

The cache

20 s
How old an entry can get before the next visit triggers a rebuild.

The CMS

An editor saves a change. The cache does not know yet.

On-demand options a webhook would call. Revalidate serves stale once. Expire makes the next visitor wait.

The visitors

The clock

Virtual time: 0.0 s

  • CMS hasv1
  • Cache holdsv1fresh, 0.0 s old
  • Rebuildidle
Visitors
0
Answered instantly
100%
Server renders
0
Oldest page shown
none

Latest visitors

Nobody yet. Press “One visitor arrives”, or turn traffic on.

The three outcomes

Hit The entry is younger than the revalidate window. Served from cache, nothing else happens.

Stale The entry is older than the window. The visitor still gets it instantly, and the first such visit starts one rebuild in the background.

Miss Nothing cached, or it was expired on demand. This one visitor waits for a full render.

What to try

  1. Publish a version, then send visitors. Count how many see the old one before the cache catches up.
  2. Drop revalidate to 5 s and set traffic to Spike. Origin renders barely move. Staleness shrinks.
  3. Press Expire now, then send one visitor. That is the cost of making an editor’s change instant.
  4. Press Purge with traffic on. One visitor waits, and everyone after them does not.
4Streaming and partial prerendering

One slow widget should never hold the whole page hostage

Change how the server sends the same four regions. Wrap the slow ones in Suspense, mark which ones read request data, and watch the HTTP response arrive piece by piece.

The shell goes first. Each Suspense boundary streams in when its data is ready.

shop.example/alta-trail-shell
Header not sent yet
Product not sent yet
Recommended not sent yet
Reviews not sent yet
First paint
145 ms2.4 s sooner
Product visible
420 ms2.1 s sooner
Everything visible
2.5 s
Header
20 ms
Product
300 ms
Recommended
1.2 s
Reviews
2.4 s
0 ms
HeaderProductRecommendedReviews
  • Nothing on screen
  • Placeholder showing
  • Content arrives

The HTTP response, as it arrives

200 OK · Transfer-Encoding: chunked0 of 4 chunks
…Shell flushed
…Product resolved
…Recommended resolved
…Reviews resolved

What streaming changes

It changes when the visitor first sees something, not how long the slowest query takes. The reviews still arrive at the same moment. The difference is that they no longer decide when everything else appears.

What it needs from you

Suspense boundaries around every slow region, fallbacks shaped like the final content so nothing jumps, and a host that does not buffer responses. The HTTP status is sent with the first chunk, so a late 404 cannot change it.

What partial prerendering adds

The shell itself no longer waits for an origin trip. Static regions come from the CDN at once, and the origin works on the dynamic holes in parallel from the moment the request arrives.

5Server Component boundaries

Where you put “use client” decides what the phone has to run

The product page below is a real-shaped tree. Some components need the browser, some need the server, and the rest could go either way. Move the boundary and watch the bundle respond.

  • reads data
  • uses state
  • has handlers
  • uses state
  • has handlers
  • reads data
  • uses state
  • uses state
  • reads data
  • uses state
  • reads data
  • has handlers
JavaScript the browser downloads and runs170 KB
28%less than shipping everything
  • Runtime, 45 KB
  • Needs the browser
  • You chose client
  • Pulled in by an import
Client components
11
Server components
7
Run and hydrate, mid-range phone
1.5 s of 2.1 s

DetailsTabs

Client

Switches between Description and Specs. Needs the browser. Uses state, so it must run in the browser.

components/DetailsTabs.tsx
'use client';
import { useState } from 'react';

export function DetailsTabs() {
  const [tab, setTab] = useState('description');
  return <div role="tablist">{children}</div>;
}

The boundary is an import graph

A client component and everything it imports end up in the bundle. That is why a small tabs component can quietly pull a 35 KB markdown parser to the phone.

Children cross the boundary for free

A server component can render a client component and pass other server components in as children. They arrive already rendered, so their code never ships. Press “Pass as children” on the pulled components above.

Locked means the framework enforces it

A component that calls useState cannot be a server component, and one that reads the database should never be a client one. Mark server code with the server-only package so an import mistake fails the build.

6Decide

Describe the page and get a defensible answer

Seven questions, the same ones a senior engineer asks before choosing. The ranking updates as you answer, and every call comes with the reasoning.

Start from
How often does the data change?

Think about the data on the page, not the code.

Is the page the same for every visitor?

Cookies, auth, location and A/B tests all count as personal.

Does search and link preview matter?

Crawlers and chat apps read the HTML they are given.

How many unique URLs are there?

This decides whether you can pre-build all of them.

How interactive is it?

This is about how much code has to run in the browser.

Is any part of the page slow to load?

A third-party call, a heavy query, a recommendation service.

Where will it run?

Some strategies need code running on a server.

7Interview round

Defend your choice out loud

12 scenarios of the kind frontend system design rounds use. Pick the strategy, then read why the other three lose. The follow-up question is the one an interviewer asks next.

Scenario 1 of 12

An internal analytics dashboard

A logged-in tool with live charts, filters and drag-and-drop widgets.

  • Behind SSO
  • Used on desktops
  • Almost every component is interactive