Skip to main content
All articles
Engineering··Updated February 20, 2025·3 min read

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%.

MC
Marcus Chen
Principal Engineer

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 window or document
  • 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:

  1. Leaf components first: Convert presentational components that had no client dependencies
  2. Page shells: Move layouts and shells to RSC, keeping interactive islands as client components
  3. Data layers: Migrate API calls to server-side fetch with explicit cache controls
  4. Gradual rollout: Feature flags let us A/B test page performance

Performance results

After migrating three production apps:

MetricBeforeAfter
Client JS340KB52KB
LCP2.1s0.7s
TTI3.4s1.2s
Hydration time890ms120ms

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.

Next.jsReactPerformanceArchitecture