React Server Components Are Not an Optimization. They're an Architecture.
Most teams who 'try RSC' enable it expecting a performance flag and find a redesign. The bundle-size win is real. It's also not the point. A working explanation of what RSC actually changes — and when that change is a bad fit.
There is a recurring pattern when teams adopt React Server Components for the first time. They read the announcement post, see the bundle-size numbers, install Next.js 15 or 16, flip on the app directory, and then spend three months annoyed.
The annoyance is real and it's the team's fault and it's not the team's fault.
It's the team's fault because the framing they came in with — "RSC is a performance optimization, I'll turn it on and pages will get faster" — is wrong. It's not the team's fault because that framing is exactly what most introductory material implies. The 40% bundle-size reduction headline. The "ship less JavaScript" tagline. The list of what RSC enables: streaming, suspense, async components. All of it sounds like a feature flag you opt into.
It isn't a feature flag. It's an architectural boundary, and the boundary is the point.
What actually changed
Here's the most useful way I've found to think about RSC after spending a year writing it.
In a classical React app — and this includes everything from pre-Next.js SPA all the way through Next.js with getServerSideProps — there is exactly one execution environment that runs your component code: the browser. The server might send pre-rendered HTML, but that HTML hydrates into components that all live and breathe in the client runtime. The mental model is "everything is a client component," with the server as an optimization layer.
RSC introduces a second execution environment that runs the component code itself. A server component is not pre-rendered HTML. It's a component whose code runs on the server, returns serializable output, and is never shipped to the browser. There is no hydration step for that component because it doesn't exist in the browser at all.
That is the change. Everything else — the bundle reduction, the data-fetching ergonomics, the suspense composition — follows from it.
This is why "I'll enable RSC for performance" doesn't work as a strategy. RSC isn't faster code; it's different code, running in a different place. To get the benefits, you have to design with the boundary in mind. To even know where the boundary is, you have to think explicitly about which components depend on the browser and which don't.
Where the friction lives
Most of the pain people report falls into a few categories, and once you see them as consequences of the boundary, they stop seeming like bugs.
"My state is gone." A server component can't use useState, can't subscribe to a store, can't read from localStorage. If you wrote a feature in client-component world and then casually deleted "use client", you discovered this in five different errors. The cure is "use client" at the leaf, not the root. State lives in the client; everything else can live on the server.
"My data fetching is a waterfall." RSC encourages async server components that fetch their own data. That's beautiful when components fetch in parallel and miserable when one component's fetch is upstream of another's. The cure is to fetch where the parallelism is naturally available — high in the tree if you can — and use Promise.all for siblings. The footgun is writing await getUser(); await getPosts(); in sequence when the two have no dependency. Your render time is now the sum of both round-trips for no reason.
"My library doesn't work." If a popular library was written before RSC existed and uses browser-only APIs at module scope, it will throw on the server. Some authors have shipped RSC-compatible builds; others haven't. Until they do, the workaround is to wrap the library in a "use client" boundary. This is fine architecturally and it does mean the library's JavaScript ships to the client, which is what RSC was supposed to avoid.
"My component organization feels wrong." This is the deepest complaint and the most important. Pre-RSC, you organized components by what they did. Post-RSC, you organize them by where they run. A <UserProfile> might have a server part (data fetch, layout, badges) and a client part (the follow button). You either split it or you don't get the bundle win for the server part.
None of these are problems with the model. They're consequences of the model, and the model is a redesign.
When RSC is the wrong call
I've seen teams adopt RSC and regret it. The regret has a pattern.
Teams optimized for rapid iteration on highly interactive interfaces. A real-time collaboration tool, an editor, a dashboard with twenty draggable widgets and live cursors. The page is "all client" by nature; almost every component needs browser state. RSC's boundary gives you almost nothing — you end up with "use client" everywhere — while introducing a complexity tax (you have to think about it) for no payoff. For these apps, the right answer in 2026 is still a client-rendered application with whatever flavor of SSR-for-first-paint you prefer.
Teams without a clear notion of which data lives where. RSC works because the server has data the client doesn't. If your architecture is "everything is in the client store, the server just persists it," RSC has nothing to compose with. The boundary will exist but it will be at the wrong level. You'll write server components that fetch their own props by calling client-shaped APIs, which defeats the purpose.
Teams that need to migrate yesterday. The single best advice in every RSC retro post I've read is "migrate incrementally, start with the least interactive page, expand outward." Teams who try to do the whole app at once usually undershoot the complexity and re-pin to the old stack three months in. The migration is real work.
When RSC is exactly right
If you're a content-heavy product — marketing pages, documentation, blog, dashboards with infrequent updates, e-commerce category pages — RSC is the best thing to happen to React rendering in five years. The components you'd otherwise need to mark getServerSideProps-ish, you just write as async function. The data fetching is co-located. The bundle the browser receives is the interactive bits, not the layout machinery.
If you have a clear data-on-server / interactivity-on-client split — and you'd be surprised how many CRUD apps actually do — RSC will let you write less code and ship less JavaScript at the same time.
For our own work, RSC was a clear win on the SLT marketing site you're reading now. It would be a misfit for OurTrail's photo grid, which is interactive enough that we'd be putting "use client" on practically every component, and we use a client-rendered shell instead.
The difference between those two cases isn't performance. It's where the data and the interactivity live. That is the architectural question RSC asks, and "I want my page to be faster" is not an answer to it.
The number
The most-cited number — "40% smaller JavaScript bundle" — is real, and it's the consequence, not the goal. You don't get it by enabling RSC; you get it by moving the right components to the server. A migration that ports the existing tree to app/ and slaps "use client" on most files will show ~5% reduction and feel like a waste. A migration that thinks about which trees genuinely need to be interactive will show 30–50% and feel like a different application.
That gap — between "enabled RSC" and "designed for RSC" — is the gap people fall into. The first is a checkbox. The second is the work.
If you're about to try it, do the work.
Field-tested on Next.js 16, App Router, on two production codebases. Mileage varies by application shape — which is the whole point.
Ask AI