Server vs Client Components in Next.js
A team adds "use client" to a layout file so one small interactive search box works, and every component that layout imports — navigation, footer, a large third-party date picker used elsewhere on the page — gets pulled into the client JavaScript bundle along with it, even though none of the rest of the page needed to run in the browser at all. The directive doesn't mark one component as interactive; it marks a boundary in the module graph, and everything on the client side of that boundary ships to the browser.
This guide covers how Server Components actually work as the App Router's default rendering model (not an optional optimization layered on top of a client-first framework), exactly what "use client" pulls into the client bundle and what it doesn't, the children-slot composition pattern that lets a Server Component render inside a Client Component without joining its bundle, and a real secret-leaking bug traced to its actual fix. This reflects Next.js's App Router model as of mid-2026 — verify against your installed version's docs before assuming an older mental model still applies.
Server Components Are the Default, Not an Optimization
In the App Router, every layout and page is a Server Component unless explicitly marked otherwise — this is the starting point, not a special mode you opt into. A Server Component can fetch data directly (close to its source, without exposing API keys to the browser), and it renders into a compact binary format called the RSC Payload, which contains the rendered output of Server Components plus placeholders for where Client Components belong and references to their JavaScript.
On first load, the browser receives three things: the prerendered HTML (an immediate, non-interactive preview), the RSC Payload (used to reconcile the client-side component tree), and JavaScript (which hydrates Client Components, attaching event handlers to make them interactive). On subsequent client-side navigations, the RSC Payload is prefetched and cached, and Client Components render entirely on the client without needing server-rendered HTML for that navigation — a meaningfully different path from the first load.
Quick reference
- Server Components are the default for every layout and page in the App Router — not an add-on to a client-rendered baseline.
- Server Components can fetch data and use secrets directly, since none of that code ships to the browser.
- First load: HTML (fast preview) + RSC Payload (reconciliation) + JS (hydration) all arrive together.
- Subsequent navigation: RSC Payload is prefetched/cached, and Client Components render client-side without new server HTML.
- Reducing client JavaScript is a direct consequence of this model — a component that never needs interactivity never needs to ship its code to the browser at all.
Remember this
Server Components are the App Router's starting point, not an optimization layered onto a client-rendered framework — the RSC Payload plus HTML plus JS pipeline is the actual default rendering path, not a special case.
The 'use client' Boundary: What Actually Gets Bundled
"use client" at the top of a file declares a boundary between the server and client module graphs — it does not mark just that one component as client-side. Once a file has the directive, every module it imports and every component it directly renders becomes part of the client bundle, which is why placing "use client" high in a tree (a layout, a large shared component) can silently pull far more into the browser bundle than the one interactive piece that actually needed it.
The important exception: this rule applies to a Client Component's module graph — what it imports and directly renders — not to Server Components passed to it as children or other props. A Server Component passed that way is rendered on the server ahead of time, and only its already-rendered output crosses the boundary; it is never pulled into the client bundle itself. This distinction is exactly what makes the children-slot composition pattern work.
Quick reference
- 'use client' marks a module-graph boundary — everything that file imports and directly renders joins the client bundle.
- Placing it high in the tree (a shared layout) can pull unrelated, unrelated-large dependencies into the browser bundle.
- The exception: Server Components passed as children/props to a Client Component are rendered server-side first — only their output crosses the boundary.
- Scope 'use client' to the smallest component that actually needs interactivity, state, or browser APIs.
- A third-party component using hooks without its own 'use client' directive needs your own thin Client Component wrapper before a Server Component can render it directly.
- Props crossing the server-to-client boundary lean on the same TypeScript discipline covered in TypeScript Patterns — a serializable prop shape is worth typing explicitly.
Remember this
'use client' marks a module-graph boundary, not a single component — everything that file imports and directly renders ships to the browser, except Server Components passed in as children or props, which stay server-rendered.
The Children Slot Pattern and Context Providers
The children slot pattern is the standard way to nest server-rendered content inside client-side interactivity without pulling the server content into the client bundle: a Client Component like <Modal> accepts children and only handles the interactive open/close state, while a Server Component like <Cart> (which fetches its own data) is passed in as that children prop from a parent Server Component — <Modal><Cart /></Modal>. <Cart> renders on the server ahead of time; <Modal> never imports it, so it's never part of <Modal>'s client bundle.
React Context is a specific case where this pattern is required, not optional: Context is not supported in Server Components at all. Using a theme or global-state context means creating a Client Component wrapper around the provider, then importing that wrapper into a Server Component layout — and rendering the provider as deep in the tree as reasonably possible (wrapping just the content that needs it, not the entire <html>/<body>), so more of the surrounding tree can stay static and server-rendered instead of being pulled into client-side reconciliation unnecessarily.
Quick reference
- Children-slot pattern: pass a Server Component as children/props to a Client Component instead of importing it into a client-marked file.
- The Server Component in the slot renders on the server ahead of time — its rendered output crosses the boundary, not its source module.
- React Context has no Server Component support — any Context Provider needs a thin Client Component wrapper.
- Render a Context Provider as deep in the tree as possible, not around the entire document, to keep more of the tree server-rendered.
- Props passed from a Server Component to a Client Component must be serializable — functions, class instances, and similar non-serializable values can't cross that boundary as plain props.
Remember this
Passing a Server Component as children into a Client Component keeps it server-rendered instead of bundled — the same pattern is mandatory for Context, since Context has no Server Component support at all.
Recover One Environment-Poisoning Secret Leak
Trigger. A data-fetching function reading process.env.API_KEY and calling an external service lives in a shared module with no boundary markers. A component elsewhere in the app imports that function directly, and that importing file happens to carry "use client" — pulling the fetch function into the client module graph. Symptom. The app doesn't fail to build. In the browser, the call to the external API returns 401s — because Next.js only includes environment variables prefixed NEXT_PUBLIC_ in the client bundle, replacing any other variable reference with an empty string, so the client-side code silently sends an empty string where the API key should be, instead of throwing a clear error anywhere.
Root mechanism. Without an explicit boundary marker on the module, nothing stops a Client Component from importing server-only code — the failure only surfaces as a confusing downstream 401, far from its actual cause. Recovery: add import 'server-only' at the top of the module containing getData() — this turns any future accidental client-side import into a clear build-time error instead of a silent empty-string secret and a runtime API failure. Verification: attempt the same import from a Client Component and confirm the build now fails immediately with a clear message, rather than succeeding and failing quietly at runtime. Prevention: mark any module handling secrets or server-only logic with server-only from the start, as a habit — not just after the first incident makes the gap obvious.
Quick reference
- Trigger: server-only code with no boundary marker gets imported into a file marked 'use client'.
- Symptom: the build succeeds; the runtime failure looks like an unrelated 401 from an external service.
- Root mechanism: only NEXT_PUBLIC_-prefixed env vars reach the client bundle — anything else silently becomes an empty string there.
- Recovery: the server-only package converts this exact mistake into an immediate, clearly-labeled build-time error.
- Prevention: mark secret-handling and server-only modules with server-only from the start, not reactively after the first silent leak.
- This is one instance of the wider server/client trust-boundary discipline covered in What Full Stack Really Means.
Remember this
An unmarked server-only module imported into client code doesn't fail the build — it silently ships an empty-string secret and fails at runtime far from the actual cause; the server-only package turns that into an immediate, correctly-labeled build error instead.
Key takeaway
Server Components are the App Router's default rendering model — every layout and page starts as one, rendering into an RSC Payload that combines with HTML and JS on first load. "use client" marks a module-graph boundary, pulling everything a file imports and directly renders into the client bundle, except Server Components passed in as children or props, which stay server-rendered through the slot pattern that Context providers specifically require. An unmarked server-only module imported into client code fails silently at runtime, far from its actual cause, unless it's explicitly marked with the server-only package.
Practice (25 min): build a layout with a Server Component logo/nav and a Client Component search box, and confirm in your bundle analyzer that only the search box's code (and its own imports) ships to the client. Build a Modal Client Component that accepts a Server Component Cart as children, and confirm the Cart's data-fetching code never appears in the client bundle. Then deliberately import a server-only secret-handling function from a Client Component without the server-only package, observe the silent runtime failure, add server-only, and confirm the same mistake now fails at build time instead. Pass when you can point to your own bundle analysis and name exactly why each piece of client JavaScript is there.
Related Articles
Explore this topic