This article is part of my Learning Patterns series, where I distill insights from Lydia Hallie & Addy Osmani’s book into real‑world React/Next.js workflows.
1 | What is Client‑Side Rendering (CSR)?
In CSR, the server ships only a bare‑bones HTML container; React and its JavaScript take over in the browser—handling routing, data fetching and templating.
Server ⟶ <div id="root"></div>
Browser ⟶ downloads `/static/js/bundle.js` ⟶ runs React ⟶ paints UI
Quick demo in a Next.js 14 app
// app/csr-clock/page.js
"use client";
import { useEffect, useState } from "react";
export default function Clock() {
const [now, setNow] = useState(Date.now());
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, []);
return <p className="text-center text-2xl">{new Date(now).toLocaleTimeString()}</p>;
}
Everything (including the interval) runs on the client, so no extra round‑trips are needed.
2 | Understanding the JavaScript Bundle
What lands in the bundle?
- Static imports in every route.
- Third‑party dependencies.
- Your own components and utilities.
The book reminds us that “the bigger the bundle, the longer users stare at a blank screen” before FCP and TTI kick in.
Peeking inside
# Install once
npm i -D @next/bundle-analyzer
# Build with analysis
ANALYZE=true npm run build
This opens an interactive treemap showing which packages bloat the bundle.
Measuring real performance
# Local Lighthouse
npx lighthouse http://localhost:3000 --view
# Web‑Vitals snippet (in _app.tsx)
import { reportWebVitals } from 'next/web-vitals';
export function reportWebVitals(metric) {
console.log(metric);
}
3 | Pros & Cons of CSR
| 💚 Strengths | 😬 Trade‑offs |
| --------------------------------------------------------------| --------------- -------------------------------------------------|
| SPA‑like fluid navigation—no full page reloads | Initial blank screen until JS downloads & executes |
| Clear separation between API & UI layers | SEO is tougher: crawlers may time‑out before content appears |
| Powerful client‑only interactions (e.g. real‑time dashboards) | Large bundles hit FCP/LCP/TTI, especially on low‑end devices |
| Works offline/with service‑workers | Potential duplication of validation logic across client & server |
4 | Five Ways to Speed‑Up CSR in Next.js
Performance is inversely proportional to bundle size—so shrink, split and delay JS!
4.1 Route‑based code splitting (automatic)
Next.js ships each page as its own chunk. Avoid giant layouts that re‑import heavy libs everywhere.
4.2 Component‑level code splitting
import dynamic from "next/dynamic";
const EmojiPicker = dynamic(() => import("./EmojiPicker"), {
ssr: false,
loading: () => <p>Loading…</p>,
});
EmojiPicker drops out of the initial bundle and loads on demand.
4.3 Tree‑shaking & dead‑code elimination
- Use ES modules (
import { X } from "y") - Prefer lightweight icon/lib alternatives (e.g.
@svgr/webpackover Font‑Awesome).
4.4 Inline critical CSS, defer the rest
Inline the bare minimum styles needed for first paint—defer bulky component CSS with @import() inside the component file.
4.5 Audit third‑party packages
Regularly compare alternatives (react-select → react-select-search, etc.) and watch Lighthouse/TBT drop.
Real‑world win (Next.js Movies App)
- Lazy‑loading a sidebar reduced Total Blocking Time by 71%.
- Swapping Font‑Awesome for inline SVG slashed LCP by 23%.
5 | Putting It All Together – A Checklist
- Analyze your bundles after every dependency addition.
-
Split routes & heavy components with
next/dynamic. - Inline only the CSS needed for FCP.
- Ship SVGs/icons smartly—avoid icon fonts.
- Monitor Web‑Vitals in production and iterate.
6 | Conclusion
Client‑Side Rendering isn’t dead—it just demands discipline. By treating every kilobyte of JavaScript as a cost and applying the patterns above, you keep CSR fast, SEO‑friendly enough, and enjoyable to build.
Inspired by pages on CSR, bundle‑splitting and performance tuning in **Learning Patterns* (Hallie & Osmani, 2021).*
Top comments (0)