跳转到主要内容

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.

ModeFirst paintBundle sizeData accessComplexity
CSRSlower (depends on network + JS)LargerClient onlyLow
SSRBetter HTML first paintSimilar to CSR (hydration still needed)Server + clientMedium
Streaming SSRBest perceived performanceSimilar to SSRServer + clientMedium–High
RSCGood (less client JS for non-interactive parts)Smaller for server-only workServer-first, client islandsHigh (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.

TypeScript
// 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.

SSR & RSC - Guides - React Docs - React 文档