Server & Client Components

Lesson 1 of 5

The Two Component Worlds

"The client and server environments have different capabilities. Server and Client components allow you to run logic in each environment depending on your use case." — Next.js Documentation, "Server and Client Components"

For about a decade, a React component was one kind of thing. It was a function that took props and returned elements, it ran in a browser, and if you were doing server-side rendering it also ran on a server — but it was the same function running twice, and the whole discipline of SSR was arranging for those two runs to agree. Everything you knew about components applied uniformly, because there was only one kind.

There are now two. They are both written as functions that take props and return elements, they live in files that look identical, and they can render each other. But they run in different places, at different times, with different capabilities, and — this is the part that takes longest to internalise — only one of them ever reaches the browser at all. A server component is not a component that runs on the server first and the client later. It is a component that runs on the server and then ceases to exist, leaving behind only its output.

That single fact reorganises everything. It is why a server component can hold a database password and a client component cannot. It is why a server component can be async and a client component cannot. It is why you can pass a number across the boundary but not a function. It is why moving 'use client' up one file can add two hundred kilobytes to your bundle, and why the fix is usually to move it down rather than to remove it.

This chapter establishes the division itself: what each world can do, what a server component actually turns into, what the browser receives on a first load versus a navigation, and what all of that costs. The next chapter takes the boundary between them and works out exactly where it falls, which is a subtler question than it looks. For now the goal is to stop thinking of "server component" as "component, rendered on the server" and start thinking of it as a different kind of thing entirely — one whose output is data, not markup, and whose code the user never receives.

The big question

If a server component's code never reaches the browser, what exactly does the browser receive in its place — and what does that leftover have to contain for React to finish the job?

Two environments, two capability sets

Start with the capabilities, because most placement decisions follow mechanically from them. The rule is not stylistic. A component belongs in the client world if and only if it needs something that only exists in a browser; otherwise it stays where it starts, which is on the server.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

The last row deserves its own emphasis, because it is where most real confusion lives. People hit a component that queries a database and has a filter input, conclude that the model does not fit their application, and mark the whole thing 'use client' — at which point the database call has to move somewhere else anyway, and they have shipped the rest of the component to the browser for nothing. The model does fit. It is telling you the component had two responsibilities.

One capability difference is worth calling out separately because it produces a confusing error rather than a design question. A client component cannot be async. Server components can be, and it is one of their headline features — const post = await getPost(id) directly in the component body. Client components cannot, because React needs to be able to re-render them synchronously at any time. When a client component needs to read something asynchronous, including the params promise from chapter two, it uses React's use() hook rather than await.

Everyday analogy

Consider the difference between a letter and a machine. If I want you to know a number, I can write it down and post it to you, and what arrives is the number — you cannot see how I calculated it, what records I consulted, or what else was on my desk. If instead I want you to be able to recalculate the number whenever something changes, I have to send you the machine: the formula, the inputs, the reference tables, everything. The machine is heavier to post and it reveals its own workings to anyone who opens the box. That is exactly the choice between a server component and a client component. Rendering a price on the server posts the price. Rendering it in a client component posts the pricing logic. Both put a number on the screen, and only one of them lets the recipient work out your margins or change the inputs and watch the answer move. Most of the skill in this part of the book is noticing which parts of an interface genuinely need to recalculate in response to the user, and sending machines only for those.

What a server component actually becomes

Here is the piece that fixes the mental model. A server component does not render to HTML and stop. It renders to the RSC payload — a compact serialised representation of the rendered tree — and the documentation is precise about what that payload contains. Three things:

  1. The rendered result of the server components.
  2. Placeholders for where client components go, plus references to their JavaScript files.
  3. Any props passed from a server component to a client component.

Modelling that structure is the fastest way to understand every rule that follows.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

Read the payload as a list and the rules stop needing to be memorised. Client components appear as references — a name and a URL to a JavaScript chunk — which is why marking a component 'use client' adds weight: you have added a chunk the browser must fetch. Props appear as literal values inside the payload, which is why they must be serialisable, and which is the subject of the third chapter of this part. And server component code appears nowhere at all, which is why it is both free and safe.

That last point has a corollary people find surprising: a heavy dependency used only in a server component costs the browser nothing. A date-formatting library, a Markdown parser, a syntax highlighter — if the only thing that reaches the client is the formatted string, the library never ships. The same import inside a client component ships in full.

A little history

The idea was presented publicly in December 2020, in a React team talk and RFC that described "Zero-Bundle-Size React Server Components", and the demo that made the case was a Markdown renderer. Rendering Markdown in the browser meant shipping a parser — a couple of hundred kilobytes to turn text the server already had into HTML the server could have produced. Rendering it in a server component shipped the HTML and none of the parser. It took nearly three years to reach a stable framework release, and the delay is worth understanding: the hard part was never rendering on the server, which React had done since 2015. The hard part was making a tree that is half server and half client reconcile correctly on the client, which required a new serialisation format — the payload above — and changes deep in React's reconciler. What strikes me looking back at the original RFC is how much of the discussion was about bundle size and how little was about the security property, which in practice is the one teams end up valuing most. "The database credentials cannot leak into the bundle because the code holding them is not in the bundle" was almost a footnote in 2020. It is now the first thing many teams cite as the reason they adopted the model at all.

First load and every load after it

The two paths through the system are different, and the difference explains a whole family of bugs — including the reason a component can render correctly on refresh and wrongly on navigation.

On a first load, three things arrive and are used in order. HTML gives an immediate non-interactive preview. The RSC payload reconciles the client and server trees. JavaScript hydrates the client components and makes them interactive. On a subsequent navigation, the HTML step is gone entirely: the RSC payload is fetched (usually already prefetched, per Part I), and client components render on the client with no server-rendered HTML at all.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

That asymmetry is the seed of the hydration chapter at the end of this part, and it is worth carrying from here: hydration mismatches are a first-load phenomenon. A component reading localStorage during render is broken, but it is only visibly broken when someone reloads the page — which, exactly as with the navigation timings in Part I, is not the path you naturally test.

What it costs, and where the boundary should sit

Now the practical consequence. Because 'use client' marks a file and everything reachable from it, the position of the directive decides the size of the bundle. The documented advice — add the directive to specific interactive components rather than to large parts of the UI — is easy to nod along to and easy to violate, because the violating version is the one that requires less thought.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

Sixty-six kilobytes for a search box versus one hundred and five for the same search box plus three components that never change. The difference is not an optimisation you apply later; it is a consequence of which file the directive is in, and it compounds, because every future import into that layout is now also a client import.

There is a related trap in the same shape. Marking a component 'use client' and then importing a heavy library into it is a decision most people would review carefully. Importing that library into a file that is already on the client side of the boundary is the same decision, and it does not look like one — nothing in the diff says 'use client', and the weight arrives silently.

The engineer's notebook

The habit I would most like to install in anyone learning this model is to read 'use client' as a cost, out loud, every time. Not "this component is interactive" but "this file and its entire import graph now ship to every visitor". Those describe the same line of code and produce completely different instincts about where to put it. The specific mistake I have made and then watched other people make is marking a layout or a provider file, because a client component somewhere under it needed context, and not noticing that the directive dragged everything alongside it into the bundle. The fix is nearly always the same and is documented explicitly: render providers as deep in the tree as they can go, and have the provider wrap {children} rather than the document. The second habit is to check whether a component genuinely needs interactivity or merely looks like it does. A dropdown that filters a list needs state. A dropdown that navigates to a URL does not — it needs a link and a form, both of which work on the server. I have removed more 'use client' directives by asking that question than by any refactor.

Keeping server code on the server

The last piece is a safety property, and it is more subtle than "server components are private". Modules can be imported by either world, so a helper written for the server can be pulled into a client component by an ordinary import that no reviewer will look at twice.

Next.js gives you two lines of defence, and it is worth knowing exactly what each one does — because one of them is a build-time error and the other is a silent substitution.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

The pairing is deliberate. NEXT_PUBLIC_ substitution protects your users; server-only protects your afternoon. Both packages — server-only and its counterpart client-only — are optional installs in Next.js, which handles the imports internally to produce clearer errors; the npm packages' actual contents are not used.

In the real world

A team I know reduced their largest route's JavaScript by a little over 40% in an afternoon, and the change touched three files. There was no clever code splitting and no library swap. Someone had put 'use client' at the top of a shared components/index.ts barrel file eighteen months earlier — entirely reasonably, because at the time most of what it exported was interactive — and every component added to that barrel since had inherited the directive by living in the same module graph. The fix was to delete the directive from the barrel and add it to the four components that genuinely needed it. What I find worth retelling is not the size of the win but how invisible the cause was to everything the team measured. Their bundle budget alerted on totals, so it fired late and pointed at whichever component happened to cross the threshold. Their code review looked at diffs, and no diff contained the problem. The defect was a single line in an unrelated file, and it had been correct when it was written. Barrel files are a common enough pattern that this is worth stating as a rule: a directive in a file that re-exports other files does not describe that file, it describes everything it re-exports, forever, including things that do not exist yet.

Common Mistakes

  1. Thinking a server component runs on the server and then again on the client. It does not. It runs once, on the server, and only its output travels. That single misconception generates most of the confusion in this part of the book.

  2. Marking a whole layout or provider 'use client' because one leaf inside it needs state. The directive covers the file and everything reachable from it, so the bundle grows by far more than the interactive part.

  3. Treating "needs a database and needs state" as an unsupported case. It is a component with two responsibilities. Split it: server component fetches, client component interacts.

  4. Making a client component async. Only server components can be. A client component reads promises with React's use() instead.

  5. Assuming a heavy dependency is expensive wherever it is used. In a server component it costs the browser nothing, because the code never ships. The same import in a client component ships in full.

  6. Testing only by navigating. Client components render twice on a first load and once on a navigation, so anything that renders differently on the server than in the browser breaks only on refresh.

  7. Relying on NEXT_PUBLIC_ stripping as your protection. It prevents the leak but converts the mistake into a confusing runtime failure elsewhere. Add server-only so the build stops at the import.

  8. Prefixing a genuine secret with NEXT_PUBLIC_ to silence a problem. That is the one action that actually publishes it — the prefix is a declaration that the value is public.

  9. Rendering context providers at the root of the document. Wrap {children} and push the provider as deep as it will go, so the static parts of the tree above it stay server-rendered.

  10. Adding an import to an existing client file without noticing it is a client file. No directive appears in the diff, and the weight arrives silently. The boundary is a property of the file, not of the line you are editing.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
What is the difference between a Server Component and a Client Component, and which is the default?
Model answer — Server Components are the default — every component is one unless a file is marked 'use client'. They run on the server only, so they can fetch from a database directly, use secrets, and be async, and their code never goes to the browser. Client Components run in the browser as well, so they can use state, effects, event handlers and browser APIs like localStorage, but their code has to be downloaded. You use a Client Component when you need interactivity, and leave everything else as a Server Component.
Mid
What is actually sent to the browser when a page of Server Components renders? Be specific.
Model answer — Not the components' code. The server renders them into the RSC payload, which contains three things: the rendered output of the Server Components, placeholders marking where Client Components go together with references to their JavaScript chunks, and the props that were passed across the boundary. On a first load the browser also gets HTML for an immediate preview and then the client JavaScript to hydrate. On a subsequent navigation there is no HTML step — the payload is fetched, often already prefetched, and Client Components render in the browser. The practical consequences are that Server Component code and its dependencies weigh nothing on the client, secrets used there cannot leak through the payload, and props that cross the boundary have to be serialisable because they are literally values inside that payload.
Senior
A team's client bundle has grown steadily for months with no obvious culprit. Every individual pull request looked reasonable. What do you look for?
Model answer — I would look for a 'use client' directive high in the tree — a layout, a provider, a shared UI barrel file — because from that point everything reachable is a client import, and every subsequent addition to that subtree joins the bundle without any diff mentioning the boundary. That is exactly the profile of "no single bad commit": the bad commit was months ago and moved the boundary, and everything since has been ordinary work landing on the wrong side of it. Concretely I would use the bundle analyser to find which entry pulls the weight, trace back to the shallowest file carrying the directive, and push it down toward the leaves that need it — often that means a provider wrapping {children} instead of the document, and splitting a component that fetches and interacts into two. I would also add server-only to the data-access modules, which turns the class of mistake into a build error rather than something you find in a bundle report a quarter later.
Company lens — how the same topic gets asked
micro1Direct and practical: here is a component that fetches and has a filter input — make it work. They are watching whether you split it or reach for useEffect and a fetch.
GoogleConceptual depth: what is in the RSC payload and why is that format necessary at all, rather than just sending HTML? Expect follow-ups on reconciliation.
MetaBoundary judgement: given a page design, mark which parts are client components and defend the smallest boundary you can draw.
AmazonCost and ownership: how would you stop bundle regressions from happening at all, given that each individual change looks fine in review?
Hands-on

Two functions covering the two decisions this chapter is about. The first places a component in a world from what it needs — including recognising the component that needs splitting. The second computes what a 'use client' directive actually costs by walking the import graph, which is the calculation that should run in your head every time you type the directive. Watch the cycle case in the second one; real dependency graphs have them.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

Summary

  • Server Components are the default. Nothing is a Client Component until a file says 'use client', so the question is always "does this need to leave the server", never "should this be a Server Component".

  • A Server Component runs once, on the server, and its code never reaches the browser. It is not the same function running in two places.

  • The RSC payload contains three things: the rendered server output, placeholders and chunk references for Client Components, and the props that crossed the boundary.

  • Server Component code and its dependencies weigh nothing on the client, which is why a heavy library used only there is free — and why the same import in a Client Component is not.

  • Secrets cannot leak through the payload, because only computed values are in it, not the code that computed them.

  • A first load renders Client Components twice — once on the server for the HTML preview, once in the browser to hydrate — while a navigation renders them once, in the browser only.

  • Hydration mismatches are therefore a first-load phenomenon, invisible to testing that only clicks between pages.

  • A Client Component cannot be async. It reads promises with React's use() instead.

  • 'use client' covers the file and everything reachable from it, so its position decides the bundle size. Push it toward the leaves; render providers deep, wrapping {children}.

  • NEXT_PUBLIC_ stripping prevents the leak; server-only prevents the mistake. The first fails confusingly at runtime, the second fails clearly at build time.

We now know there are two worlds and roughly where things belong. What we have not pinned down is exactly where the line falls — and it is not simply "files with the directive". The next chapter, The 'use client' Boundary, works out what the directive marks, why a Server Component passed as children to a Client Component stays on the server, and why the module graph rather than the render tree is what decides.

Editorial reflection

What I keep noticing about this model is that it takes something the industry had spent fifteen years turning into an implementation detail and makes it a first-class thing you declare. Where code runs used to be a deployment concern, decided by which folder a file was in and forgotten thereafter; the whole trajectory of isomorphic JavaScript was toward pretending the boundary was not there. Server Components move in the opposite direction, and it is a genuine reversal, not a refinement. I think the reason it works is that the boundary never actually went away — it was just unmarked, so it got crossed accidentally, and the accidents took the form of secrets in bundles and megabytes of parser shipped to render text the server already had. Making it a directive means you can be wrong about it, which people initially experience as a burden. But being able to be wrong is what being able to be right requires: an unmarked boundary cannot be reviewed, cannot be linted, and cannot be reasoned about in a design discussion. There is a broader lesson in that for anyone designing systems, and it is not that explicitness is always better — it plainly is not, or we would all still be writing memory management by hand. It is that a boundary with real consequences on both sides is a bad candidate for hiding, and this particular boundary has security on one side and performance on the other, which makes it about the worst thing you could ask a developer to forget.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.