React Server Components explained: what reaches the browser
React Server Components in Next.js 16, in a live demo: what stays on the server, what ships as JavaScript, and what the HTML and RSC payload carry.

React Server Components are components whose code runs only on the server. In the Next.js App Router, every page and layout is one by default, and the browser gets what they render, as HTML and again in the RSC payload, never their code. Only a file that starts with "use client", plus everything it imports, ships as JavaScript. Those Client Components are still rendered to HTML on the server first, then hydrated in the browser. Data crosses from server to client only through serializable props, and a Server Component passed in as children stays server output even inside a Client Component. We built one demo that shows all of this on Next.js 16.4.0 with React 19.3.0, and nine Playwright tests check the behaviour this post shows in it. In your own app, run patched versions: the React and Next.js RCE fixes of October 2026 cover the same layer.
What are React Server Components?
React's own reference describes Server Components (opens in a new tab) as components that render ahead of time, "in an environment separate from your client app or SSR server". In Next.js that environment is the server, at build time or per request. You never mark a component as a Server Component: there is no directive for Server Components (opens in a new tab), and "use server" marks Server Functions. What you mark is the other side, with "use client".
The Next.js guide to the server and client boundary (opens in a new tab) puts the difference in one table. Here it is with what that means when you write the code:
| Server Component | Client Component | |
|---|---|---|
| Marked by | nothing (the default in app/) | "use client" at the top of the file |
| Renders on the server | yes | yes, to HTML |
| Runs in the browser | no | yes, it hydrates there |
| Its code and imports ship as JavaScript | no | yes |
useState, useEffect, onClick | no | yes |
| Can read secrets, files, a database | yes | no: treat it as browser code |
What reaches the browser: HTML, the RSC payload and client JavaScript
On a first load in Next.js (opens in a new tab), the docs name three things the browser uses: HTML for a fast, non-interactive first paint, the RSC payload to reconcile the server and client trees, and JavaScript to hydrate the Client Components. The RSC payload is a compact, serialized form of the rendered Server Component tree. It holds their rendered output, placeholders for the Client Components with references to their JavaScript files, and every prop passed from a Server Component to a Client Component.
The demo's Server Component signs the request time with an HMAC key. The key and Node's crypto module sit in a separate file that starts with import "server-only":
import "server-only"; import { createHmac } from "node:crypto"; // A made-up key, public in this repo. In an app it comes from process.env. Either way it lives// only in the server module graph: import "server-only" turns an import from a "use client"// file into a build error.const SIGNING_KEY = "lab-demo-key-stays-on-the-server"; /** The first 12 hex characters of an HMAC-SHA256 of `value`, made with Node's crypto module. */export function sign(value: string): string { return createHmac("sha256", SIGNING_KEY).update(value).digest("hex").slice(0, 12);}The card that uses it is an async function with no directive:
import { io } from "next/cache"; import { sign } from "./signing";import styles from "./rsc.module.css"; // Server Components: no directive, so they stay in the server module graph. The browser// receives what they render (as HTML and in the RSC payload), never this code. const renderedOn = () => (typeof window === "undefined" ? "server" : "browser"); export async function ServerCard() { // With Cache Components on, io() keeps what follows out of the build's static shell, // so the server signs a fresh time on every request. await io(); const signedAt = new Date().toISOString(); return ( <section className={styles.card} aria-labelledby="server-component"> <h2 id="server-component">Server Component</h2> <dl className={styles.rows}> <div> <dt>Rendered on</dt> <dd>{renderedOn()}</dd> </div> <div> <dt>Signed at</dt> <dd> <time dateTime={signedAt}>{signedAt.slice(11, 19)} UTC</time> </dd> </div> <div> <dt>Signature</dt> <dd> <code>{sign(signedAt)}</code> </dd> </div> </dl> </section> );} // Passed to the Client Component as children: it renders here, on the server, and the// Client Component only places its output.export function ServerNote() { return <span>{renderedOn()}, passed in as children</span>;}The tests check what actually arrives, on a production build:
- The HTML response holds the signature in its markup, and the same signature again inside an inline
<script>in that response: that is the RSC payload, since the RSC payload ships with the HTML (opens in a new tab) on the first load. - The signature is an HMAC-SHA256, with the key from
signing.ts, of the full ISO timestamp, date and milliseconds included, which sits in thedatetimeattribute of the<time>element; the page shows only its hours, minutes and seconds. The test reads that attribute and computes the HMAC again itself. - While the page loads, hydrates and takes three clicks, the key is in none of the responses: not the HTML, not the payload, not a JavaScript file.
- A string from
counter.tsxis in a JavaScript file. The strings that exist only inserver-card.tsxandpage.tsxare in none.
You can check the same on your own app. Open the page source and search for a value a Server Component rendered: it appears in the markup and again inside a script. Then search the loaded JavaScript files in DevTools for a string that only a Server Component's source contains: it should not be there.
How to tell which code stays on the server
One rule decides it: code crosses the boundary through imports (opens in a new tab). A file with "use client" is an entry into the client module graph, and every module it imports is pulled into the client bundle, with or without its own directive. A file no client file imports stays on the server.
The demo's Client Component is a counter:
"use client"; import { useState, useSyncExternalStore, type ReactNode } from "react"; import styles from "./rsc.module.css"; const subscribe = () => () => {}; // A Client Component: "use client" puts this file and everything it imports in the browser's// JavaScript. It still renders to HTML on the server first, then hydrates in the browser.export function Counter({ children }: { children: ReactNode }) { const [count, setCount] = useState(0); // The server snapshot (false) is used on the server and during hydration; once hydrated, // React renders again with the browser's snapshot (true). const hydrated = useSyncExternalStore(subscribe, () => true, () => false); return ( <section className={styles.card} aria-labelledby="client-component"> <h2 id="client-component">Client Component</h2> <dl className={styles.rows}> <div> <dt>Rendered on</dt> <dd>{hydrated ? "browser, after hydration" : "server, not hydrated yet"}</dd> </div> <div> <dt>Count</dt> <dd> <output aria-label="Count">{count}</output> <button type="button" onClick={() => setCount((n) => n + 1)}> Add one </button> </dd> </div> <div> <dt>Children</dt> <dd>{children}</dd> </div> </dl> </section> );}The "Rendered on" row shows that a Client Component runs in both places. With JavaScript off, the browser shows only the server's HTML, and the row reads "server, not hydrated yet" with the count at 0. With JavaScript on, it reads "browser, after hydration". It switches through useSyncExternalStore (opens in a new tab), whose getServerSnapshot is used only on the server and during hydration. React's hydrateRoot docs (opens in a new tab) list a typeof window check in render as a common cause of hydration errors, because the server and the browser would render different output.
The way to make the server side hold is import "server-only" (opens in a new tab) in every module that must never reach the browser. Importing such a module into a Client Component is then a build-time error, not a quiet leak. In Next.js you don't need to install the package: Next.js handles the import itself. Environment variables follow a similar rule: only the ones prefixed with NEXT_PUBLIC_ are included in the client bundle, and Next.js replaces the rest with an empty string.
Server Components inside Client Components: the children pattern
The page puts a second Server Component, ServerNote, inside the counter, as children:
import { Suspense } from "react"; import { Counter } from "./counter";import { ServerCard, ServerNote } from "./server-card";import styles from "./rsc.module.css"; // The page is a Server Component too: pages and layouts are, unless a file says "use client".export default function Page() { return ( <div className={styles.stack}> <Suspense fallback={<p className={styles.pending}>Signing on the server…</p>}> <ServerCard /> </Suspense> <Counter> <ServerNote /> </Counter> <p className={styles.hint}> Add one changes the count in the browser and leaves the signature as it is. <a href="">Reload</a> and the server signs a new time. </p> </div> );}On a hydrated page, the note still reads "server, passed in as children", and three clicks re-render the counter without changing it. The docs describe why: a rendered React element is serializable, so the Server Component's output crosses the boundary as data, and the Client Component only places it. Server Components passed as children (opens in a new tab) are rendered on the server ahead of time, and the RSC payload carries their result. Page owns ServerNote (its source contains the JSX), so ServerNote renders on the server; Counter is only its parent in the rendered tree. By the docs' rule, importing ServerNote into counter.tsx instead would pull server-card.tsx into the client module graph, with signing.ts behind it, and import "server-only" would turn that into a build error.
The same pattern is how you use React context: context is not supported in Server Components (opens in a new tab), so a provider is a Client Component that takes children, and the server-rendered tree goes inside it.
Is RSC the same as SSR?
No, and the demo shows both at once. The counter's HTML comes from the server: that is server-side rendering, and server-side rendering predates RSC (opens in a new tab), as the boundary guide notes; Next.js does it at build time or per request. The counter is still a Client Component, because its code ships and runs again in the browser. "Server-rendered" says how the HTML was made. "Server Component" says where the component's code runs and whether it ships.
When the server renders is a separate question. The signed time in the demo is per request: the reload test sees a new one after a reload. The lab runs with Cache Components (opens in a new tab) on, and the await io() reference (opens in a new tab) explains what that means for a value like new Date(): you capture it once with use cache, into the static shell, or keep it out of the static shell with await io(), which the card does inside <Suspense>.
Why a click doesn't reach the server, and a reload does
Three clicks on Add one set the count to 3 with no network request at all, and the time, the signature and the note stay as they were. The boundary guide says a Server Component can render again when Next.js renders the route again, for example on a navigation, a refresh or after revalidation. In the demo, a state change in the browser re-renders the Client Component without any request to the server and leaves the Server Components' output as it was.
Reload and the server runs again: the test sees a later time, a new signature and the count back at 0, because a full load starts the Client Component from its initial state.
How to split a Next.js page into Server and Client Components
Start with Server Components
Pages and layouts in
app/are Server Components by default. Fetch data and read secrets there, with no directive.Put use client on the smallest interactive part
Add
"use client"to the file that needs state, effects, event handlers or browser APIs, such as a button, a filter or a form field, not to the page around it. Everything that file imports ships too.Mark server modules with server-only
Add
import "server-only"to every module that reads a secret, a database or the file system. A wrong import then fails the build.Pass small, serializable props
Send a Client Component only the fields it shows: strings, numbers, plain objects, arrays, Dates, Maps, Sets, Promises and JSX. Never pass a whole database record, and never pass a plain function: only Server Functions cross the boundary.
Pass server UI as children
When server-rendered UI must sit inside a Client Component, such as a modal, a tab panel or a context provider, pass it as
childrenor another prop from a Server Component instead of importing it.Check a production build
Run
next buildandnext start, open the page source and search for a server-only string in the loaded JavaScript, as the demo's test does.
What breaks the server and client split
Props are sent to the browser
Every prop a Server Component passes to a Client Component is serialized into the RSC payload. The Next.js data security guide (opens in a new tab) shows the usual leak: a page passes a full user row to a "use client" profile component, and every column of that row reaches the browser, even the ones the component never renders. Return a small object with only the public fields from a server-only data layer. React's taint APIs can block a specific object or value from reaching the client (in Next.js behind experimental.taint (opens in a new tab)), but the guide treats them as an extra layer, not a replacement for filtering.
Functions don't cross
A plain function can't be a prop from a Server Component to a Client Component: React's list of serializable props (opens in a new tab) allows only Server Functions, and leaves out class instances and objects with a null prototype too. So an onClick defined on the server can't work. Define handlers inside the Client Component, or pass a Server Function marked with "use server", which crosses as a reference.
"use client" too high up
"use client" on a layout or a page moves everything below it into the client graph: each import and each component that file renders directly. The Next.js docs (opens in a new tab) recommend putting it on the interactive leaf, such as a search box, and keeping the layout around it a Server Component.
Third-party components without the directive
A library component that uses useState but has no "use client" errors when a Server Component renders it. The docs' fix for third-party components (opens in a new tab) is a file of your own that has the directive and re-exports it.
React Server Components vulnerabilities: keep React and Next.js patched
React's 19.3.1 release lists one change under React Server Components, "Improve transport protocol" (opens in a new tab). On 14 October 2026, React published the critical advisory Unauthenticated Remote Code Execution in React Server Components (opens in a new tab). It names the packages react-server-dom-webpack, react-server-dom-turbopack and react-server-dom-parcel, from 19.0.0 to 19.3.0, patched in 19.0.9, 19.1.10, 19.2.9 and 19.3.1; the react package itself is not on its list, so check those packages in your lockfile too. The same day, Next.js 16.4.1 (opens in a new tab) fixed three advisories of its own. The dev digest of 18 October 2026, linked at the top and under Further reading, lists what each one covers and which version to take.
The demo was verified on Next.js 16.4.0, inside the range of the Next.js advisories, with React 19.3.0. We have not re-run it on the patched versions yet; build your own app on them.
Are React Server Components the same as SSR?
No. SSR describes how HTML is produced: on each request, as opposed to at build time. A Server Component is about where the component's code runs and whether it ships to the browser. Client Components are server-rendered to HTML too, then hydrated with their JavaScript; a Server Component's code never reaches the browser.
Do React Server Components reduce bundle size?
Their own code and what they import stay out of the client bundle, so a heavy library used only in a Server Component costs the browser nothing to download. Client Components still ship, with everything they import, and the RSC payload adds the rendered output as data to the page. The saving depends on how much of the page can stay on the server.
Can Server Components use useState or useEffect?
No. Server Components are not sent to the browser, so they cannot use state, effects or event handlers. Put the interactive part in a small Client Component marked with 'use client' and render it from the Server Component.
Are Server Components stable in React 19?
Yes. React's documentation says Server Components in React 19 are stable and will not break between minor versions. The lower-level APIs that bundlers and frameworks use to implement them do not follow semver, which is why frameworks such as Next.js pin a React version.
What is the difference between Server Components and Server Actions?
A Server Component renders UI on the server and has no directive. A Server Action (a Server Function) is a function marked with 'use server' that the browser can call, for example from a form. 'use server' never marks a Server Component.
