The model in one paragraph
In an app that supports React Server Components (RSC), components are Server Components by default. They render on the server, can read data directly, and their code is not sent to the client. A module becomes client code when it starts with the 'use client' directive, and so do all the modules it imports, whether or not they have the directive themselves (source 1). Only client modules are bundled for and evaluated in the browser (source 1). The line between the two worlds is drawn by imports, not by where components appear in the rendered tree.
Where the performance gains come from
Less JavaScript. Code for Server Components, including any libraries they use only to produce markup, stays on the server. A markdown parser, a date library used for formatting, or a syntax highlighter used to render static code samples need not ship at all if only Server Components use them.
Less hydration. Server Components produce output that the client displays but does not hydrate as interactive components. Only Client Components hydrate. A page that is mostly content with a few interactive widgets hydrates only those widgets.
Data near the source. Server Components can fetch data on the server, close to databases and APIs, instead of the browser making requests after load. That removes client-side request waterfalls from the critical path.
The boundary decides everything
Because 'use client' pulls in the whole import graph beneath it, where you place it matters more than any other RSC decision.
Consider a product page. If the top-level layout file is marked 'use client' because it needs one piece of state for a menu, everything it imports becomes client code: the product description renderer, the reviews list, the formatting utilities. The page then behaves much like a traditional client-hydrated app, with RSC adding complexity but little benefit.
Moving the directive down to a small MenuToggle component, and keeping the layout as a Server Component that renders it, limits the client bundle to the toggle and its own imports.
// layout.jsx: Server Component (no directive)
import MenuToggle from './MenuToggle';
import ProductDetails from './ProductDetails';
export default function Layout({ product }) {
return (
<>
<header><MenuToggle /></header>
<ProductDetails product={product} />
</>
);
}// MenuToggle.jsx: the only client module here
'use client';
import { useState } from 'react';
export default function MenuToggle() {
const [open, setOpen] = useState(false);
return <button aria-expanded={open} onClick={() => setOpen(!open)}>Menu</button>;
}Passing Server Components through Client Components
A Client Component can still contain server-rendered content if that content is passed in as props, typically children. The React reference gives this example: a Server Component passed as children to a Client Component still renders on the server (source 1). This pattern lets an interactive wrapper, such as a collapsible panel, hold a large static body without pulling the body into the bundle.
Serialization costs
Values passed from Server Components to Client Components must be serializable: primitives, arrays, maps, sets, dates, plain objects, Server Functions, JSX elements and promises are supported, while class instances and ordinary functions are not (source 1). Every such value is serialized into the payload the client receives.
That payload is a real cost. If a Server Component passes an entire API response to a Client Component that displays three fields, the full response travels to the browser. Pass only what the client needs.
Common performance traps
- Client boundaries too high. The most common problem, described above. Audit which files carry
'use client'and what they import. - Shared utilities imported from client modules. A large utility module imported once by a client file ships whole, unless the bundler can tree-shake it well.
- Large props across the boundary. Oversized serialized data inflates the HTML and slows the first byte to paint.
- Waterfalls on the server. Sequential
awaitcalls in nested Server Components can create server-side request waterfalls. Start independent requests in parallel and use Suspense boundaries so slow data does not block the whole page. - Everything dynamic. Reading request-specific data high in the tree can force a whole route to render per request, losing the benefit of caching. Frameworks such as Next.js offer explicit caching to keep shared parts static.
An invented audit of client boundaries
Picture a dashboard built with the App Router. A search through the code finds 'use client' in 46 files. Grouped by what they import, three stand out: a layout file that imports the navigation, a data table and a charting wrapper; a shared Card component used on every page that imports an animation library; and a form helper that imports a full validation library.
The plan for such a case: split the layout so only the navigation toggle is a client module; make Card a Server Component and move the animation into a small client wrapper used only where cards animate; and load the validation library only in the forms that need it. A bundle analyzer run after each step shows the shared client chunk shrinking, and a throttled trace confirms less hydration work. The file counts in this example are invented; the method is what matters.
Measuring the server side
Moving work to the server can move the bottleneck there. Watch time to first byte under realistic load, check that independent data requests run in parallel, and make sure slow requests sit behind Suspense boundaries so they stream rather than block. A Server Component that is fast in development against a local database may be slow in production against a remote API.
Profiling RSC apps
Server and client both need attention.
- Client side. Record a Performance trace on a throttled CPU and look at bundle evaluation and hydration. React 19.2 adds Scheduler and Components tracks to Chrome DevTools, showing which components mount and which updates were blocking or ran as transitions (source 2).
- Bundle composition. Use your bundler's analyzer to see which modules ended up in client chunks and why. Our bundle analysis guide covers the tools.
- Server side. Measure server render time per route and time to first byte under load. Suspense boundaries and streaming help users see content while slow parts finish.
Newer pieces: Activity and Partial Pre-rendering
React 19.2 added two features that affect performance in RSC apps (source 2):
<Activity>splits an app into sections that can be visible or hidden. Hidden sections keep their state but unmount effects and defer updates, which allows pre-rendering likely next screens in the background without competing with the visible one.- Partial Pre-rendering APIs prerender the static parts of a page ahead of time and resume rendering later to fill in dynamic content. Frameworks build on these; Next.js exposes the idea through Cache Components (source 3).
What React 19.3 adds
React 19.3, released on September 9, 2026, does not change the core advice in this guide: keep client boundaries low, pass little data across them and measure on throttled devices (source 4). A few additions are still relevant to performance work:
- Rendering
<Context>directly in Server Components. A Server Component can import a Context from a'use client'module and render it with a value, without a separate client Provider wrapper. That removes one common reason for adding an extra client component high in the tree (source 4). browser()fromreact-dom. Callinguse(browser())in a component makes it suspend on the server, so the nearest Suspense fallback goes into the HTML and the component renders only on the client. It is a cleaner way to keep browser-only code out of server rendering than checkingwindowduring render, which is a classic cause of hydration mismatches (source 4).<Activity>support in the Server Components payload, plus several fixes for hidden Activity trees (source 4).- Independent Transitions. The changelog notes that Transitions now render independently, so a slow Transition no longer holds up unrelated ones (source 4).
- Stable View Transitions and Fragment Refs.
<ViewTransition>animates only Transition updates, not urgent ones, so it should not slow typing or clicks, but animations still cost frame time on slow devices and are worth checking in a trace (source 4).
Next.js 16.4 ships with React 19.3 (source 5).
A checklist for RSC apps
- List every file with
'use client'and note what it imports. - Push client boundaries down to the smallest interactive components.
- Pass server-rendered content into client wrappers as
childrenwhere possible. - Pass only the data the client needs across the boundary.
- Run independent server data fetches in parallel; wrap slow ones in Suspense.
- Check the client bundle with an analyzer after changes.
- Confirm with traces on a throttled CPU and with INP and LCP field data.
Sources
- 'use client' directive, React reference Primary source (React team)
- React 19.2, React blog Primary source (React team)
- Next.js 16.3, Next.js blog Primary source (Vercel)
- React 19.3, React blog (September 9, 2026) Primary source (React team)
- Next.js 16.4, Next.js blog (October 6, 2026) Primary source (Vercel)