TL;DR / Server Components render exclusively on the server, sending only their output (not their JavaScript) to the client, eliminating bundle size cost for non-interactive UI.
How It Works
┌──────────────┐ RSC ┌──────────────┐ ┌───────────┐
│ Server │ payload │ Client │ │ Browser │
│ Component │───────────→│ Component │───────────→│ DOM │
└──────────────┘ └──────────────┘ └───────────┘
DB queries, fs useState, onClick
access, secrets interactivity
Server Component JS never ships to client.
Only its rendered output crosses the wire.
React Server Components (RSC) introduce a new component type that runs exclusively on the server. Unlike traditional SSR where all components render on the server and re-render on the client during hydration, Server Components render on the server only. Their JavaScript definition — the component function, its imports, its dependencies — never appears in the client bundle.
The RSC Payload
When a Server Component renders, it does not produce HTML directly. It produces an RSC payload: a serialized description of the rendered component tree, represented as a JSON-like streaming format. This payload describes what the Server Component rendered — its output elements, props for Client Components, and serialized data — but not how it was rendered.
The client receives this payload and reconstructs the component tree. Server Components become plain rendered output (equivalent to static HTML elements). Client Components in the tree receive their props from the payload and hydrate normally. The key insight: the client never needs the Server Component's code because it already has the rendered result.
What Server Components Can Do
Because Server Components run on the server, they have direct access to server-side resources:
- Database queries — call your ORM or run SQL directly in the component, no API route needed.
- File system access — read files, parse markdown, load configuration.
- Environment secrets — use API keys, database credentials, and tokens without exposing them.
- Large dependencies — import heavy libraries (syntax highlighters, date formatters, markdown parsers) without adding them to the client bundle.
A Server Component that imports a 200KB Markdown parser adds 0KB to the client bundle. The parser runs on the server, and only the rendered HTML crosses the wire.
The "use client" Boundary
Server Components are the default in the RSC model. Components that need interactivity — state, effects, event handlers, browser APIs — must be explicitly marked with "use client" at the top of the file. This directive creates a boundary: the component and everything it imports becomes part of the client bundle.
The boundary is a file-level concept, not component-level. When you add "use client" to a file, all components exported from that file become Client Components. Server Components can render Client Components by passing them as children or through props, but Client Components cannot import Server Components directly (they can receive them as children or as serialized props).
Data Flow and Serialization
Server Components pass data to Client Components through props, but those props must be serializable. You can pass strings, numbers, booleans, arrays, plain objects, Dates, Maps, Sets, and other Server Components (as JSX). You cannot pass functions, class instances, or Symbols because these cannot be serialized across the server-client boundary.
This serialization constraint shapes how you architect RSC applications. Data fetching happens in Server Components and flows down as serialized props. Interactivity happens in Client Components that receive that data. This creates a natural separation: Server Components are the "data layer" and Client Components are the "interaction layer."
Performance Model
The performance benefit is twofold. First, bundle size decreases because server-only code is excluded. Second, server rendering is faster because Server Components do not need to produce hydration-compatible output — they never hydrate. The client skips them entirely during the hydration pass.
On subsequent navigations in a framework like Next.js App Router, Server Components re-render on the server and send a new RSC payload. The client merges this payload with the existing tree, preserving Client Component state. This means navigating between pages does not reset form inputs, scroll positions, or other client state — a major improvement over traditional full-page SSR.
Framework Integration
Next.js App Router is the primary production-ready RSC implementation. All components in the app/ directory are Server Components by default. Remix and other frameworks are exploring RSC adoption but with different integration approaches. The RSC protocol itself is framework-agnostic — it defines how server output is serialized and streamed to the client.
Gotchas
- Server Components cannot use hooks. No
useState,useEffect,useRef,useContext, or any custom hook that depends on them. These require a client runtime that Server Components do not have. - Props must be serializable. Passing a callback function from a Server Component to a Client Component is not possible. Use Server Actions (
"use server") for server-side mutations triggered by the client. - The
"use client"boundary cascades. If a Client Component imports another component, that component is also a Client Component regardless of whether it uses client features. Plan your import graph carefully. - Server Components re-execute on every request. They have no persistent state between renders. If you need caching, use framework-level caching (
"use cache"directive in Next.js) or HTTP cache headers. - Debugging is harder because part of the component tree exists only on the server. Browser DevTools cannot inspect Server Component internals — only their serialized output.