Server Rendering, SSG and React Server Components
|
This section documents React 19.x for the web, using function components and Hooks throughout (class components appear only where React still requires them, such as error boundaries). React Native is out of scope and gets only a pointer. The React 19 additions follow the current official React documentation. This content was generated with the assistance of AI and should be verified against react.dev before being relied on in production, since React APIs continue to evolve between releases. This section’s bibliography lists the reference material consulted while preparing these pages. |
Where the initial HTML is produced — in the browser, on a server per request, or ahead of time at build — shapes an app’s time-to-first-byte, SEO, and hosting model. This page is a brief, example-led tour; a framework (Next.js, React Router) implements these for you.
CSR vs. SSR vs. SSG vs. ISR
| Strategy | How | Optimizes for |
|---|---|---|
CSR (client-side) |
Server sends a near-empty HTML shell + JS; the browser renders everything. |
Simple static hosting; highly dynamic, auth-gated apps. Poor SEO, slow first paint. |
SSR (server-side) |
Server renders full HTML per request, browser hydrates it. |
SEO + fast first contentful paint for dynamic, per-user pages. Needs a running server. |
SSG (static generation) |
HTML rendered once at build time, served from a CDN. |
Fastest TTFB, cheapest hosting. Content that changes rarely (docs, marketing, blog). |
ISR (incremental) |
SSG + background revalidation after a TTL or on demand. |
Static speed with periodically fresh content (product catalog, news). |
Hydration
Server-rendered HTML is inert. Hydration attaches React’s event listeners and state to that existing markup instead of recreating it.
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('root'), <App />);
The tree React renders on the client must match the server HTML. Common hydration mismatches: rendering
Date.now() / Math.random() / window during render, locale-dependent formatting, and invalid nesting
(<div> inside <p>). See
hydrateRoot.
react-dom/server
-
renderToPipeableStream— Node streams; the standard SSR entry point. Streams HTML as it renders and supports<Suspense>. -
renderToReadableStream— the same for Web Streams (edge runtimes, Deno, workers). -
renderToString/renderToStaticMarkup— synchronous, no streaming; legacy or for tiny cases (emails, static fragments). -
prerender/prerenderToNodeStream— render to a fully-formed static HTML string for SSG.
With streaming SSR, content wrapped in <Suspense> is sent as a fallback first, then the real markup is
streamed in and swapped when its data resolves — the user sees the shell immediately. See
Server React APIs.
React Server Components and Server Functions
Server Components render only on the server: their code and data-layer dependencies never reach the
browser bundle. Client Components (marked 'use client' at the top of the file) are the interactive
islands that hydrate. Server Functions (marked 'use server') are callable from the client and run on the
server — the mechanism behind Actions.
// ProductPage.jsx — a Server Component (no directive needed in an RSC framework)
import db from './db';
import AddToCart from './AddToCart'; // a Client Component
export default async function ProductPage({ id }) {
const product = await db.product.find(id); // runs on the server
return (
<article>
<h1>{product.name}</h1>
<p>{product.price}</p>
<AddToCart productId={product.id} /> {/* interactive island */}
</article>
);
}
// AddToCart.jsx
'use client';
import { useState } from 'react';
export default function AddToCart({ productId }) {
const [added, setAdded] = useState(false);
return <button onClick={() => setAdded(true)}>{added ? 'Added' : 'Add to cart'}</button>;
}
Next.js (App Router) is the reference implementation — see nextjs.org/docs/app.
React 19 also hoists document metadata (<title>, <meta>, <link>) rendered anywhere into <head>, and
adds resource-preloading APIs (preload, preinit, prefetchDNS, preconnect). References:
Server Components,
'use client',
'use server'.
Worked example A — SEO-friendly e-commerce
Goal: product pages must return full HTML (name, description, price, JSON-LD) on the first response so Googlebot indexes them, while volatile pieces (live stock, "customers also bought", reviews) load client-side afterward.
// app/products/[id]/page.jsx — Server Component: indexable HTML on first byte
import db from '@/lib/db';
import LiveStock from './LiveStock';
import Recommendations from './Recommendations';
export async function generateMetadata({ params }) {
const p = await db.product.find(params.id);
return { title: p.name, description: p.summary };
}
export default async function Page({ params }) {
const p = await db.product.find(params.id); // rendered on the server
return (
<article>
<script type="application/ld+json" dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org', '@type': 'Product',
name: p.name, description: p.summary, offers: { '@type': 'Offer', price: p.price },
}),
}} />
<h1>{p.name}</h1>
<p>{p.summary}</p>
<p className="price">{p.price}</p>
<LiveStock sku={p.sku} /> {/* 'use client' — AJAX after hydration */}
<Recommendations productId={p.id} /> {/* 'use client' — AJAX after hydration */}
</article>
);
}
// app/products/[id]/LiveStock.jsx
'use client';
import { useQuery } from '@tanstack/react-query';
export default function LiveStock({ sku }) {
const { data } = useQuery({
queryKey: ['stock', sku],
queryFn: () => fetch(`/api/stock/${sku}`).then((r) => r.json()), // client-side AJAX
refetchInterval: 30_000,
});
if (!data) return <p>Checking availability…</p>;
return <p>{data.inStock ? `${data.count} in stock` : 'Out of stock'}</p>;
}
A pure CSR SPA would serve <div id="root"></div> and fill the product data in with JavaScript; crawlers that
do not execute JS (or execute it on a delay) then see an empty page, and ranking suffers for pages whose value
is the content. Server-rendering the stable parts fixes that; the dynamic parts stay client-side where
freshness matters. Cross-links: Data Fetching,
SEO with Semantic HTML.