SSR & RSC
If you need faster first paint, better SEO, or a smaller client bundle, rendering strategy becomes an architectural decision.
The important shift is: you are no longer only choosing “where React renders”, you are choosing boundaries—what runs on the server, what runs on the client, and what data crosses between them.
Migration strategy
Migrations fail when teams start from the leaves. Start at the route boundary, then push server-only work downwards.
- Stabilize route-level data loading and loading/error boundaries first.
- Then move server-only computation and data shaping to the server incrementally.
Capability matrix
Treat `SSR`/`streaming`/`RSC` as tools with different costs. The “best” choice depends on your product goals and team constraints.
Use this table to pick a baseline, then refine with profiling and UX requirements.
| Mode | First paint | Bundle size | Data access | Complexity |
|---|---|---|---|---|
| CSR | Slower (depends on network + JS) | Larger | Client only | Low |
| SSR | Better HTML first paint | Similar to CSR (hydration still needed) | Server + client | Medium |
| Streaming SSR | Best perceived performance | Similar to SSR | Server + client | Medium–High |
| RSC | Good (less client JS for non-interactive parts) | Smaller for server-only work | Server-first, client islands | High (boundary discipline) |
Boundaries: serialization and security
The boundary is both a performance tool and a security tool. Treat it like an API: explicit, validated, and versionable.
- Server-only code can access secrets and private networks; never ship it to the client.
- Only pass serializable data across the boundary; keep “fat objects” and connections on the server.
- Treat the boundary as an API contract: validate inputs and sanitize outputs as needed.
Hydration cost: keep interactivity scoped
`SSR` can still be slow if the client has to hydrate a large tree. Prefer “server for structure, client for interactions”.
In practice this means: render the bulk of content on the server, and carve out small client islands for inputs, toggles, and navigation.
// Server-side (route/page boundary)
export default async function Page() {
const res = await fetch('https://example.com/api/products', { cache: 'no-store' });
const products = await res.json();
return (
<div>
<h1>Products</h1>
<ClientFilters initialCount={products.length} />
<ul>{products.map((p: any) => <li key={p.id}>{p.name}</li>)}</ul>
</div>
);
}
// Client-side (interactive island)
// 'use client';
function ClientFilters({ initialCount }: { initialCount: number }) {
// useState / useEffect / event handlers live here
return <div>Count: {initialCount}</div>;
}Common pitfalls
- Mixing client-only stateful UI with server-only data logic without clear boundaries.
- Assuming `SSR` automatically fixes performance—network waterfalls and heavy `hydration` can still hurt.
Production checklist
- Sensitive data stays server-side; client bundles contain no secrets.
- `hydration` scope is intentional; large non-interactive areas avoid client JS.
- Route-level loading and error boundaries exist, with actionable recovery.
- Caching strategy is explicit: what is cached, where, and how invalidation works.
Further reading
If you want to go deeper, pick one direction: data boundaries, component boundaries, or the underlying model.
- Next: Data Fetching (caching boundaries, waterfalls, invalidation).
- Learn: Server Components (what runs where; constraints and migration).
- Model: Server Components and Concurrency & Suspense.
- Reference: Suspense.