Skip to the content.

Client-side rendering is not the villain, it is a trade-off

Client-side rendering is not the villain, it is a trade-off

Client-side rendering — the single-page app that ships a mostly-empty HTML shell and builds the whole UI in the browser — has become the thing everyone apologises for. It gets blamed for slow first paints and bad SEO, and both criticisms are fair. But they are fixable trade-offs, not a verdict, and for a large class of apps CSR is genuinely the simplest and cheapest choice. Understanding CSR well means being honest about both halves: what it costs on the first load, and what it buys you afterwards.

CSR pays a slow first load then serves fast in-app navigations A timeline: the first load is a long bar (download bundle, then render). Subsequent navigations are short bars because no full page reload happens. first load download bundle render nav 1 render nav 2 render no reload — data fetch only instant client-side transition
CSR front-loads the cost: one slow start to download and boot the app, then fast in-app navigations with no full reloads.

The shell is empty on purpose

A CSR app’s initial HTML is deliberately minimal — a root node and a script tag. Everything the user sees is created by JavaScript after the bundle loads and boots:

<!doctype html>
<div id="root"></div>            <!-- nothing here yet -->
<script src="/app.js"></script>  <!-- this builds the entire UI in the browser -->

That is the source of the two complaints: before app.js downloads and runs, there is nothing to paint (slow first contentful paint) and nothing for a crawler that does not execute JavaScript to read (SEO gap). Both are consequences of the same design choice — rendering in the client.

After boot, navigation is the payoff

Once the app is running, CSR shines. Navigating between routes does not reload the page or re-download HTML; the router swaps components and fetches only the data the new view needs, so transitions feel instant:

// no full page reload — the router renders the next view in place
<Link to="/dashboard">Dashboard</Link>;   // swaps components, fetches just the data

function Dashboard() {
  const { data } = useQuery(["stats"], fetchStats);  // only the new data crosses the wire
  return <Stats data={data} />;
}

For an app where the user logs in once and then works for twenty minutes — a dashboard, an editor, an internal tool — that snappy in-app feel is the whole experience, and the one-time slow boot barely registers.

Fix the trade-offs, or choose another strategy

The two costs have known mitigations. The slow first paint shrinks with code splitting (ship only the first screen’s JS), a meaningful loading skeleton (so the shell is not blank), and prefetching the next route. The SEO gap closes with prerendering for crawlers or moving the public, indexable pages to SSR/SSG while the authenticated app stays CSR. The honest decision rule: choose CSR when the app is behind a login, highly interactive, and SEO-irrelevant, and reach for SSR or SSG when the first view matters to a stranger or a crawler. CSR is not the villain — it is the right tool when the first paint is not the thing you are optimising for. The render-strategy-choice exercise makes you weigh exactly these factors per page, and the infinite-scroll exercise is a taste of where CSR’s in-app responsiveness genuinely wins.