We committed to React Server Components in April of last year. Four production projects, all on Next.js 14 and then 15, all shipped, all still running. Time to give them a grade. Spoiler: it's a B minus, with an asterisk that says "depends entirely on who's maintaining the code."
The wins are real. The losses are also real. The thing nobody told us at the start is that the losses compound, and the wins do not.
Where they paid off
The TTFB wins are not theoretical. On our largest project — a marketplace-style site with about 11,000 listings — we measured a drop from 740ms median TTFB on the old Pages Router setup to 320ms on the App Router with server components doing the listing fetches. That's a real number we can put in a client report. Lighthouse Performance scores went from a consistent 78–82 to a consistent 91–94 on mobile. The client noticed. So did Google: indexing throughput on the listing pages roughly doubled in the first month after launch.
The JavaScript bundle deltas were also significant. The marketplace project shed about 280 KB of client JS by moving data-fetching and rendering of the listing cards to the server. The detail pages dropped from a 410 KB initial bundle to about 190 KB. Some of that was unrelated refactoring, but the bulk was server components doing work that used to ship to the client.
For content-heavy projects the model genuinely is the right one. You fetch on the server, you render to HTML, you stream it down, the user sees stuff fast, and only the genuinely interactive bits ship as client components. When it works, it works. We'd ship that pattern again.
Where it stopped working
The mental model is brutal for anyone newer to React. We onboarded a contractor in November who'd been writing React for four years. She was productive on day one with our client components and absolutely lost in the server boundary. The error messages do not help. "You're importing a component that needs useState" is a clear message until you realize the import was three files deep and the actual problem is that a util being imported transitively included a client-only module. Debugging that on a Friday afternoon is not fun work.
The "use client" / "use server" boundary is leaky in ways the docs gloss over. We've had at least four production bugs that traced back to someone passing a non-serializable prop across the boundary — a function, a Date in some cases, a Map. The error is usually loud in development and silent in production until a specific edge case trips it. We now have a lint rule that flags suspicious prop shapes. We did not have that for the first six months and we paid for it.
The data-fetching footgun
Server components make it dangerously easy to call your database from anywhere in your component tree. This is genuinely convenient. It is also a great way to end up with eleven queries to the same table because three different components in the same page all decided to fetch the user record. Without component-level memoization or a request-scoped cache, you will fan out database calls in a way that looks fine in dev and lights up your database in production.
Next.js has the cache() primitive and React has request memoization. Both work. Both require discipline. We've now standardized on a thin data-access layer that wraps all DB calls through a request-scoped memoizer, which feels like we've reinvented the world of Apollo client-side caching except on the server. There's a lesson in there.
Build times got worse
This was the surprise. We expected server components to slow down our dev loop slightly. They slowed it down a lot. The marketplace project's cold start in dev is about 31 seconds. Hot reload of a single server component is around 2.5 seconds. The old Pages Router version of the same project cold-started in about 11 seconds and hot-reloaded in under a second. We've squeezed some of that back with turbopack, but turbopack itself was unstable enough through Q3 last year that we burned a week chasing build-only failures.
Production build times tripled. Eight minutes on Vercel for a clean build, where the old Pages Router version did the same project in around two and a half. We can live with this — it's CI time, not user time — but it makes the client-facing iteration cycle measurably slower.
The honest verdict
For a content-heavy site with real performance constraints, where the team is going to stay in the React ecosystem long-term and someone is going to own the mental model, we'd use server components again. For a small-business marketing site or a brochureware project, we genuinely would not. The complexity tax is too high relative to what the client gets out of it.
If we'd shipped any of the four projects on plain Next.js Pages Router with aggressive ISR and lazy-loaded client islands, three of them would have looked nearly identical to the user. Only the marketplace project would have suffered, and only on TTFB. That's a real outcome, but it's the kind of outcome you should be deliberate about choosing. We were not deliberate. We chose the new thing because it was the new thing.
The recommendation I'll defend hardest: if you're picking a React stack for a client today, the decision is no longer "App Router vs Pages Router." It's "do we actually need React at all, and if we do, are we sure the marginal complexity of server components pays back inside the lifetime of this project?" Half the time the second answer is no, and we've learned to say so out loud before we start.