StrategiesGENERALNETWORK-SPECIFIC

Client-Side Rendering

The server sends a shell, the browser downloads a bundle, runs it, then asks for data. Everything a person can read sits behind that chain.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

What has to finish before a client-rendered page shows anything, and who is excluded while it waits?

The user intent

Someone follows a link expecting to read or do something. The first thing the browser hands them is an empty document.

The obvious build

Ship one index.html with a mount point and a script tag. It is a static file, it deploys to any CDN with no server runtime, and the framework builds the entire interface once the code runs. One template, one code path, no server rendering to keep in sync.

Why it breaks

The document has nothing in it to read. Until the bundle has downloaded, parsed and executed, the viewport shows whatever colour the stylesheet gave the body (The Critical Rendering Path).

How it breaks in a real browser
  • The document has nothing in it to read. Until the bundle has downloaded, parsed and executed, the viewport shows whatever colour the stylesheet gave the body (The Critical Rendering Path).
  • Data cannot start arriving until code runs, because the code is what knows the URL. That is two dependent round trips in series on a connection where latency, not bandwidth, is usually the constraint (Reading a Network Waterfall).
  • A blank document exposes no headings, no landmarks and no text. A screen-reader user who lands on it is handed a page containing nothing, with nothing announcing that more is coming (The Accessibility Tree).
  • A crawler or link-preview fetcher that does not execute script sees the shell. What gets indexed, and what appears when the link is pasted into a chat, is a mount point (The Head: Metadata That Changes Rendering).
  • When the bundle fails — a chunk that 404s after a deploy, a syntax error on an older engine, a blocked third-party script that took the page down with it — the result is not a degraded page. It is no page (Frontend Error Tracking).
  • On a low-end phone the dominant cost is often not the download but the parse and execute, and that cost is paid on the one thread that could otherwise be painting (The Real Cost of JavaScript).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • The server's answer is a document with no content in it: a head, a stylesheet link, an empty container and one or more script tags. That document is identical for every route and every user, which is exactly why it is cheap to serve and cacheable at an edge (CDN Delivery).
  • The parser builds a tiny DOM and the preload scanner discovers the script and stylesheet references written into the markup — but nothing it cannot see, and it cannot see a URL your code computes at runtime (The Preload Scanner).
  • When the bundle executes, the framework mounts: it builds a component tree in memory, produces DOM nodes and appends them under the mount point. This is the page's first render, and it is a full construction rather than a reconciliation against existing markup (Reconciliation and Keys).
  • Only then do the components that need data run their fetches. The request the server could have made before it sent a byte is instead made by a browser that has already spent a round trip and an execution to learn it needed to (The Life of a Fetch).
  • A second render commits the data into the DOM, style and layout run over a tree that is now substantially bigger, and the first pixels a person can read are produced (The Rendering Pipeline).
  • There is no hydration step, and that is the one structural advantage of the shape: nothing is rendered twice, no markup has to match, and no serialized state has to be embedded and re-parsed (Hydration).

What this makes the browser do

And which of it is avoidable.

  • Download, parse, compile and execute the whole application bundle before a single application pixel exists. Parse and compile scale with bytes; execution scales with what the code does at module scope (Bundle Analysis).
  • Construct the entire DOM subtree under the mount point from scratch, then style and lay out a document that went from near-empty to full in one commit (What a Mutation Costs).
  • Run the framework runtime itself — scheduler, reconciler, effect queue — which is main-thread work that exists before your first component does (Reactivity Models).
  • Avoidable: everything for routes this visitor has not opened. Route-level splitting turns one gate into a smaller gate plus a later one (Code Splitting).
  • Avoidable: the second round trip, if the data request can be started from the HTML — a preload hint, or an inline script that fires the fetch before the bundle arrives (Resource Hints).

What first paint is waiting on

The useful way to read this timeline is not "it is slow". It is that every milestone except the first is downstream of the bundle, and the bundle is downstream of a document that contains no content. Nothing here can be reordered by working harder on the client, because the ordering is a data dependency rather than a scheduling choice.

Note where Hydration sits. There is nothing to hydrate: the mount is the first and only render of the tree. That is the genuine structural advantage of client rendering and it is worth naming, because every other strategy in this module pays for early content with a second pass over the same tree.

One route, client-renderedrelative units, comparable to the other profiles in this module and to nothing else
HTML arrives
JS downloaded
Hydration
Data ready
Content visible
Interactive
  • HTML arrivesThe fastest document in the module, because nothing had to be produced. It is also empty, so the speed bought no information.
  • JS downloadedThe whole gate. Every kilobyte here is on the critical path to first content, not merely to interactivity.
  • HydrationNothing to hydrate. The framework mounts and builds the tree once — the only strategy here that renders the page a single time.
  • Data readyThe request begins only now, because the code that knows the endpoint had to run first. A second round trip, in series behind the first.
  • Content visibleThe latest content arrival of the five strategies, and the first moment the page is readable by anyone at all.
  • InteractiveContent and interactivity land together. The shell was technically responsive earlier, but there was nothing on it to act on.

Units are ordering, not duration. The teaching is the shape — two serial network dependencies with an execution between them — which survives any device and any connection.

The shell is a promise the browser cannot keep yet

It is worth looking at the actual bytes, because the honest version of the critique is not "the HTML is small" — small is good — but "the HTML contains no information". A crawler, a feed reader, a link unfurler and a screen reader all read this document, and all of them read the same thing: nothing.

Some of that is recoverable without changing strategy. A real title, a description, an h1, the site navigation as ordinary markup and a correctly-sized skeleton all cost a handful of bytes and turn an empty document into a partially useful one. None of it makes the page interactive earlier — that is what the rest of the module is about — but it changes what happens during the wait.

The document a client-rendered route actually serves
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <title>App</title>
6 <link rel="stylesheet" href="/assets/app.4f2c.css">
7 <script type="module" src="/assets/app.9b31.js"></script>
8 </head>
9 <body>
10 <div id="root"></div>
11 </body>
12</html>
13
14<!-- the same document, made useful during the wait -->
15<body>
16 <a class="skip" href="#main">Skip to content</a>
17 <header><nav aria-label="Main"><!-- real links, real markup --></nav></header>
18 <main id="main" aria-busy="true">
19 <h1>Orders</h1>
20 <p class="visually-hidden" role="status">Loading orders</p>
21 <div class="skeleton-row"></div>
22 <div class="skeleton-row"></div>
23 </main>
24 <div id="root"></div>
25</body>

The second version is still client-rendered and still waits on the same bundle. What changed is that the document now has a title, a heading, a landmark, working navigation and an announced pending state — so the wait is informative rather than empty.

Where client rendering is the right answer

This strategy has a bad reputation it only half deserves. Its weaknesses are concentrated in one place — the first view of a route that someone or something needs to read without running code — and outside that place, its simplicity is a real engineering asset.

The question that separates the two cases is not "is this app modern". It is: is the first view of this route personal, is it behind a login, does anything that cannot execute JavaScript need to read it, and how long is a typical session. Answer those four and the choice usually makes itself (Choosing a Rendering Strategy).

SituationClient renderingWhy
A dashboard behind a loginGood fitThe first view is personal, so it could not have been pre-built and could not have been cached publicly anyway. The blank period is paid once per session by a user who is already committed.
An editor, canvas or consoleGood fitAlmost every pixel needs script regardless. Server rendering would produce markup that hydration immediately re-derives, paying twice for nothing.
A marketing or documentation pagePoor fitThe entire payload exists to render text that could have been in the HTML, and the audience includes crawlers and previews that will never run it (Static Site Generation).
A product page that must be indexablePoor fitWhat gets indexed is the shell. This is the one failure mode with no client-side mitigation (Server-Side Rendering).
A long session with many navigationsGood fitOne entry cost amortises across client-side navigations that never touch the document again (Client-Side Routing).
A first-visit-heavy site on low-end devicesPoor fitParse and execute dominate, and they are paid before anything is readable rather than after (The Real Cost of JavaScript).

How to build it

Most important first.

  • Decide first who has to read this route without running script. If the honest answer is "search engines, link previews, and anyone whose bundle did not load", client rendering is the wrong shape for that route and probably the right shape for the ones behind the login (Choosing a Rendering Strategy).
  • Split by route and load the rest on navigation, so the gate in front of first content is the code for one view rather than for the application (Lazy Loading).
  • Start the data request before the bundle can. A link rel=preload for the endpoint, or a few inline lines that begin the fetch and stash the promise, collapses two serial round trips into two overlapping ones (Resource Hints).
  • Put something real and correctly-shaped in the shell: the header, the navigation, the page title, and a skeleton whose boxes are the size the content will be, so arrival does not shove the layout (Visual Stability).
  • Make the loading state a first-class, announced state rather than an absence. An empty region and a pending region look the same to the eye and are completely different to a screen reader (Loading, Error, Empty — The States You Did Not Render).
  • Cache the shell aggressively and the bundle by content hash, so returning visits skip the download entirely and only pay execution (Content-Hashed Assets).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • The blank period is not neutral. A screen-reader user who arrives during it is presented with a document that has no headings to navigate by, no landmarks, and no text — and unlike a sighted user, they get no visual cue that anything is in progress (The Accessibility Tree).
  • Put the real document title and a heading in the served HTML, not only in the rendered tree. The title is announced on navigation and is the first thing many users hear (The Head: Metadata That Changes Rendering).
  • A pending region should say it is pending. A container with aria-busy while loading, or a politely-announced status message, converts an absence into information (Live Regions and Announcement).
  • After the client router takes over, a route change does not reload the document, so nothing is announced by default. Every subsequent navigation needs a deliberate focus move or announcement or the page silently swaps under a screen-reader user (Focus Management).
  • Client rendering also delays the point at which keyboard users have anything to tab to. Focus order is empty during the wait, and if focus was in the browser chrome it stays there — which is fine, as long as arriving content does not steal it (Keyboard Operability).

What can go wrong

Failure modes
  • A chunk that no longer exists. The shell is cached, it references a hashed file from the previous deploy, and the user gets a blank page with a network error nobody sees (Long-Lived Clients and Version Skew).
  • A JavaScript error during mount. Because everything renders from one root, a throw in one component can leave the entire document empty rather than one region broken — which is why an error boundary around routes is structural rather than defensive (Frontend Error Tracking).
  • The mitigation failing: a skeleton whose boxes are the wrong size trades a blank page for a page that jumps, which some users find worse (Visual Stability).
  • A third-party script in the head that blocks parsing, so the bundle is discovered late and the blank period is extended by a vendor you do not control (Third-Party Scripts and the Supply Chain).
  • Prefetching the data from the HTML and then fetching it again from the component, because the two paths do not share a cache key (Five Components, One Request).
What can arrive out of order
  • A user clicks a link in the static shell before the router has mounted. The click either hits a plain anchor and triggers a full document navigation, or hits nothing at all, depending on how the shell was written.
  • Two components mount and each fetch the same resource, producing duplicate in-flight requests unless something deduplicates by key (Five Components, One Request).
  • A prefetched data promise from the HTML resolves after the component that would have used it already started its own request, so the saving is spent twice (Out-of-Order Responses).
Security
  • The bundle is public. Every endpoint, feature flag name, internal role string and comment in it is readable by anyone who opens the network panel, minification included (The Browser Is a Runtime).
  • Because the shell contains nothing personal, it is genuinely safe to cache publicly — the one caching question client rendering makes easy, and the one server rendering makes hard (Browser HTTP Caching).
  • Every authorization decision still belongs to the server. Rendering the whole UI on the client makes it tempting to treat "the component did not render" as a control, and it is not one (Authorization-Aware UI).
  • A blank shell plus a permissive script policy is still an injection surface: the sinks are in the client renderer rather than in a server template, and they are just as real (Cross-Site Scripting).
Misreads
  • "It is a single-page app, so the first load is slow and everything after is fast." The second half is only true if navigation does not download a new chunk and fetch new data, which it usually does.
  • "Crawlers run JavaScript now, so this is solved." Some do, on their own schedule and budget, and link-preview fetchers, feed readers and many other consumers do not (The Head: Metadata That Changes Rendering).
  • "A spinner covers the gap." A spinner tells a sighted user that something is happening. It tells a crawler nothing and, unless it is announced, it tells a screen reader nothing either.
  • "Client rendering means no server." It means no rendering server. The data still comes from somewhere, and that somewhere is now on the critical path twice over.

Measuring it, and what changes in the field

How you would see this
  • The network waterfall shows the shape directly: document, then bundle, then a data request that starts only after script execution. Two serial dependencies is the signature (Debugging the Network).
  • The performance trace shows a long script block with no paint inside it, followed by a large style-and-layout pass. Content arrival sitting after that block is the diagnostic (A Mental Model of the Devtools).
  • Disable JavaScript and load the route. Whatever is on screen is what a non-executing client gets, and it is the most honest ten-second test in this module.
  • Field data separates devices that pay execution cost from devices that do not. A local profile on a development machine systematically understates this strategy's worst case (Real User Monitoring).
Slow device, slow network, large data, old tab
  • On a high-latency connection, the two serial round trips dominate and reducing bytes barely moves the blank period. The number of dependencies is the lever, not their size (Reading a Network Waterfall).
  • On a low-end device, parse and execute dominate instead, and the same bundle produces a much longer wait for a reason no waterfall shows (The Real Cost of JavaScript).
  • On a long session, the calculus inverts: one slow entry amortises across hundreds of instant client-side navigations, which is why applications behind a login often choose this deliberately (Client-Side Routing).
  • On a second visit with a warm cache, the download disappears and only execution remains — so any measurement that only looks at repeat visits will not see the problem at all (Browser HTTP Caching).
What this costs
  • The simplest deployment story in this module — a bucket and a CDN, no server runtime, no build-time page list — bought with the worst first-visit experience and no story at all for clients that do not execute script.
  • No duplicated rendering work and no markup contract to keep, bought with the requirement that every pixel wait for the bundle.
  • Splitting by route fixes the size of the first gate and introduces a second one at navigation time, which needs its own loading boundary to avoid feeling like a stall (Route Loading Boundaries).

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe dependency chain — document, then script, then data, then pixels — follows from the fact that the URL of the data lives inside the code, and holds in every browser and every framework that renders from an empty mount point.
  • NETWORK-SPECIFICWhich half of the wait dominates depends on the connection: on a high-latency link the serial round trips dominate and byte reduction barely helps, while on a fast link the same page is bounded by parse and execute instead, so the two situations call for opposite fixes.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Testing & Reliability Engineering: a route whose only content path is JavaScript needs a test that asserts what a non-executing client receives, which is a different assertion from any component test.