Key takeaways
- Server Components are not SSR — they never hydrate and never ship to the browser as JavaScript.
- The bundle-size win comes from dependencies that stay on the server entirely, not from smaller component code.
- The hard part is the boundary: once you mark a component
'use client', everything it imports joins the bundle. - Props crossing the boundary must be serialisable, which changes how you structure composition.
React Server Components are the most significant change to React's mental model since hooks, and also the most frequently misunderstood. The confusion is understandable — the name collides with server-side rendering, which is a different thing that React has done for a decade.
Server Components are not server-side rendering
Traditional SSR renders your component tree to HTML on the server, sends that HTML, and then sends the same components as JavaScript so React can attach event handlers. That second step is hydration. The component code is transmitted twice in effect: once as markup, once as the JavaScript needed to make it interactive.
A Server Component is never sent to the browser at all. It executes on the server, produces a serialised description of its output, and that description is streamed to the client. There is no hydration because there is nothing to hydrate — the component does not exist client-side in any form.
This is why the two can coexist. A Server Component can render a Client Component, which is then hydrated normally. SSR is about when HTML is produced; Server Components are about where the code lives.
Where the bundle savings actually come from
The commonly cited benefit is smaller bundles, but the mechanism is worth being precise about, because it determines which refactors pay off.
The saving is not primarily the component's own code — components are small. The saving is the dependency graph. A component that formats dates with a large i18n library, parses Markdown, highlights syntax, or queries a database pulls all of that into the client bundle under the old model. As a Server Component, none of it ships. The library executes on the server and only the rendered output crosses the wire.
The practical implication: the components worth converting first are the ones with heavy, non-interactive dependencies. Converting a simple presentational component saves almost nothing.
The boundary is the whole design problem
This is where teams get into trouble. The 'use client' directive does not mark a single component as client-side — it marks an entry point into the client graph. Every module that component imports, transitively, becomes part of the client bundle.
The common failure mode looks like this: a large layout component needs one interactive dropdown, so someone marks the layout 'use client'. Every child, and every child's dependencies, are now client code. The Server Component benefit evaporates while the codebase still carries the added complexity.
The pattern that works
Push the boundary as far down the tree as possible, and pass Server Components through Client Components as children rather than importing them inside.
A Client Component that receives children does not import those children — it renders whatever it is given. So a client-side interactive shell can wrap server-rendered content without pulling that content into the bundle. Structuring composition this way is what keeps the client graph small as the application grows.
Serialisation constraints
Props passed from a Server Component to a Client Component must be serialisable, because they genuinely cross a network boundary. Primitives, plain objects, arrays, dates, and JSX elements all work. Functions, class instances, and closures over server-only values do not.
This constraint is not arbitrary friction — it makes the client/server split explicit rather than implicit, and it catches at build time what would otherwise be a runtime surprise. But it does mean callback-passing patterns need rethinking. The usual answer is Server Actions, which give you a function reference that is safe to pass because invoking it triggers a server round trip rather than executing client-side.
What you give up
- No hooks in Server Components. No
useState, nouseEffect, no context — they have no lifecycle and no client state. - No browser APIs. No
window, no event handlers, no direct DOM access. - Debugging spans two environments. A failure can originate on either side of the boundary, and the tooling for tracing across it is still maturing.
When it is worth it
Server Components pay off most in content-heavy applications with substantial data-fetching and modest interactivity — documentation sites, e-commerce catalogues, dashboards, publications. They pay off least in highly interactive applications where nearly every component needs client state anyway.
The honest framing is that this is an architectural commitment rather than an optimisation you enable. Adopted deliberately, it removes a large amount of JavaScript from the critical path. Adopted carelessly, it adds a boundary to reason about and delivers a bundle no smaller than before.
Comments (2)
Alex Thompson
65w ago
Incredible analysis. The points about multimodal reasoning are spot on — this is exactly the kind of deep dive we need to understand these models properly.
Nour Al-Rashid
65w ago
Great article! I appreciate the balanced approach — acknowledging both the capabilities and the safety considerations. Looking forward to your follow-up piece.