Why we moved everything to Next.js 15 Server Components
An honest assessment of React Server Components in production: what works, what breaks, and how we reduced client JS by 84%.
Six months ago, we bet our entire frontend architecture on Next.js 15 App Router and React Server Components. It was not a casual decision. We had production Next.js 12 and 13 apps with thousands of pages, and the migration path was unclear.
Here is what we learned.
The promise
Server Components let you render UI on the server without shipping component JavaScript to the client. For content-heavy pages, this is transformative. Our marketing site went from 340KB of client JS to 52KB. Not compressed. Not gzipped. Actual parsed and executed JavaScript.
The LCP dropped from 2.1s to 0.7s. CLS went to zero because we stopped hydrating empty shells.
The reality
Server Components are not free. They introduce new constraints:
- No
useState,useEffect, or any client hooks - No browser APIs
- No direct access to
windowordocument - Interleaving server and client components requires mental model shifts
The biggest issue we hit was data fetching. In the Pages Router, getServerSideProps was straightforward. In App Router, you fetch directly in components, but caching semantics are subtle. We accidentally cached user-specific data because we misunderstood cache: 'force-cache'.
Our migration strategy
We did not migrate everything at once. We used a strangler fig pattern:
- Leaf components first: Convert presentational components that had no client dependencies
- Page shells: Move layouts and shells to RSC, keeping interactive islands as client components
- Data layers: Migrate API calls to server-side
fetchwith explicit cache controls - Gradual rollout: Feature flags let us A/B test page performance
Performance results
After migrating three production apps:
| Metric | Before | After |
|---|---|---|
| Client JS | 340KB | 52KB |
| LCP | 2.1s | 0.7s |
| TTI | 3.4s | 1.2s |
| Hydration time | 890ms | 120ms |
What still breaks
- Third-party libraries: Many React libraries assume a client environment. We had to fork two charting libraries.
- Error boundaries: Server Component error handling is still primitive. A thrown error in a deeply nested Server Component can crash the entire page.
- Debugging: Stack traces across the server/client boundary are confusing.
Should you migrate?
If you are building content-heavy sites, dashboards, or internal tools: yes. The performance gains are real and significant.
If you have heavily interactive applications with complex client state: evaluate carefully. The App Router is production-ready, but the ecosystem is still catching up.
We are all-in. The tradeoffs are worth it for our use cases, but go in with open eyes.