Hook — 04 : 45 a.m., Chiang Mai ↔ Chicago pairing session
My fan rattled as I watched Liam share his screen from a snowy Chicago apartment. He’d deployed our new marketing site—but Lighthouse showed a blank screen for nearly four seconds on 3G. “Dude, it’s client-side only,” I said, waving at the flame-chart gaps. We cracked open Next.js, moved one page intoapp/
, hit React 18’srenderToPipeableStream
, and redeployed. Now the first headline rendered in 600 ms, long before the bundle finished hydrating. Liam’s sigh of relief echoed across 12 time-zones. That midnight rescue is today’s blueprint: you’ll learn why server-side rendering (SSR) matters, how to spin it up in Next.js, and where React 18’s streaming powers fit in.
Why SSR Is Having a Renaissance
SPAs once ruled, but three forces flipped the script:
Pressure | Pain Point | SSR Advantage |
---|---|---|
Core Web Vitals | Client-only pages delay First Contentful Paint | HTML streams immediately |
SEO & Social | Bots struggle with JS-only sites | Crawlers see pre-rendered markup |
Accessibility | Screen readers wait for hydration | Static HTML is usable instantly |
Next.js ships an opinionated SSR pipeline atop React 18, giving you out-of-the-box routing, code-splitting, and data fetching that works on both server and client.
Concept Check — SSR vs. SSG vs. CSR
- CSR (Client-Side Rendering): Entire UI renders in the browser. Fast after load, slow initial paint.
- SSR (Server-Side Rendering): The server renders HTML on each request, then React hydrates on the client.
- SSG (Static Site Generation): HTML pre-built at deploy time. Great for blogs, not for user-specific dashboards.
In React 18 + Next.js app/
router, SSR is enabled by default with streaming—meaning the server can flush chunks (<Suspense>
) while waiting for slow data.
Project Bootstrap — Five-Minute Setup
bashCopyEditnpx create-next-app@latest my-ssr-demo --typescript --eslint
cd my-ssr-demo
npm run dev
Open http://localhost:3000
—you’re already rendering via next dev
, but let’s build a dynamic page to feel the power.
Step-by-Step Walkthrough
1 — Create a Server Component
Inside app/
router (Next 13+):
tsxCopyEdit// app/page.tsx
import { Suspense } from 'react';
import Users from './users';
export default function Home() {
return (
<main className="p-6">
<h1 className="text-2xl mb-4">Streaming Demo</h1>
<Suspense fallback={<p>Loading users…</p>}>
{/* Server Component */}
<Users />
</Suspense>
</main>
);
}
Users
will fetch data on the server and stream HTML while the client JS loads.
2 — Fetch Data in a Server Component
tsxCopyEdit// app/users.tsx
import 'server-only'; // Ensures this never ships to client
async function fetchUsers() {
const res = await fetch('https://jsonplaceholder.typicode.com/users', {
next: { revalidate: 60 }, // ISR: cache for 60 s
});
return res.json();
}
export default async function Users() {
const users = await fetchUsers();
return (
<ul className="space-y-2">
{users.map(u => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
Line-by-line:
server-only
directive forces bundler to exclude this module from the client chunk.fetch
withrevalidate
enables incremental static regeneration (ISR) on Vercel.- HTML streams as soon as
<li>
s are ready—thanks to React 18’srenderToReadableStream
under the hood.
3 — Hydration on the Client
No extra code! Next.js injects a tiny payload telling React 18 to hydrate your page once the JS chunk arrives, adding interactivity.
Where getServerSideProps
Still Fits (Pages Router)
If you’re on the older pages/
router:
tsxCopyEditexport async function getServerSideProps() {
const res = await fetch('https://api.example.com/items');
const items = await res.json();
return { props: { items } };
}
export default function Items({ items }) {
return <pre>{JSON.stringify(items, null, 2)}</pre>;
}
This blocks until data resolves (no streaming). Upgrading to app/
unlocks concurrent streaming—motivating teams to migrate.
Common Pitfalls & How to Fix Them
Issue | Symptom | Fix |
---|---|---|
Bundled secrets | API keys leak in client JS | Keep fetch in Server Components or getServerSideProps |
window undefined | Code crashes during SSR | Guard with typeof window !== 'undefined' |
Heavy client bundle | Long hydration time | Split with next/dynamic , lazy load charts |
Pitfall 4: Forgetting
use client
at top of interactive components—causes hydration mismatch. Always declare"use client"
for components needing state or effects.
Remote-Work Insight 🛰️
Sidebar (~140 words)
Our design team in Kenya tests pages on low-end phones. We added Next.js “visitor-latency” header in logs to compare TTFB between regions. Turns out our Tokyo edge lagged due to blocking CMS fetches. Switching those routes to server components withrevalidate
made TTFB consistent worldwide—fewer midnight pings from QA channels.
Performance & Accessibility Checkpoints
- Lighthouse → Server Response Time: Aim < 200 ms. If slower, cache with ISR (
revalidate
). - Cumulative Layout Shift: Use
next/image
and CSS aspect ratios; preload fonts in_document.tsx
. - Screen Reader Flow: Ensure dynamic content updates are announced; wrap live updates with
role="status"
once hydrated. - Coverage Tab: Confirm unused JS bytes < 50 %. Use
next/dynamic
with{ ssr:false }
for rarely used widgets.
Useful CLI & Concept Table
Tool / Concept | One-liner Purpose |
---|---|
next build && next start | Production build with SSR |
npx @next/bundle-analyzer | Inspect chunk size |
middleware.ts | Edge logic before SSR |
next/dynamic | Client-side lazy imports |
Diagram Idea (describe for SVG)
Title: “HTML Stream Timeline”
Axes: Time (x) vs. Browser Phases (y)
Bars:
- Server sends shell HTML →
<h1>
appears. - Server streams users list (chunked) →
<li>
s paint gradually. - JS chunk downloads.
- React 18 hydrates — interactivity attaches.
Wrap-Up
Server-side rendering in Next.js shifts work from the client’s slow device to your beefy servers and global edges. With React 18 streaming, you deliver meaningful HTML in milliseconds, hydrate when ready, and keep Google—and your Kenyan QA team—smiling. Start with a fresh Next.js app, migrate one page, measure, then iterate. Drop your SSR victories or horror stories below; I’ll reply somewhere between airports and asynchronous PR reviews.