Back to blog
Web2 min read

Why We Migrated a 200k-User SaaS from CRA to Next.js App Router

April 26, 2026

When Fluxen’s dashboard started shipping features weekly, our Create React App setup became the bottleneck. Cold builds crept past 90 seconds, bundle sizes ballooned past 2 MB, and Lighthouse scores on the marketing pages hovered in the 50s. Moving to Next.js App Router wasn’t a weekend project — it took six weeks and touched every corner of the codebase.

The trigger

The final straw was a client demo where the app took 8 seconds to become interactive on a 4G connection. We’d been papering over performance with skeleton loaders and optimistic UI, but we were losing the race. Next.js offered server components, streaming SSR, and built-in image optimisation — exactly what we needed.

What we migrated first

We started with the public-facing pages (marketing, pricing, docs) because they had the clearest performance story. Moving those to static generation with generateStaticParams cut time-to-first-byte from ~600 ms to under 80 ms. The wins were immediate and visible.

The authenticated dashboard was harder. We had hundreds of components with useEffect-heavy data fetching. We adopted a “server shell, client leaf” pattern: layout and navigation became server components; interactive widgets stayed as client components. This reduced the client bundle by 40%.

What broke

Context providers that lived at the app root needed to move inside a 'use client' boundary wrapper. Several third-party libraries (a charting lib and a drag-and-drop package) threw hydration errors until we wrapped them in dynamic imports with ssr: false.

Results after 8 weeks in production

  • Lighthouse performance score: 54 → 91
  • JS bundle (initial): 2.1 MB → 780 KB
  • Build time (CI): 94 s → 31 s
  • Support tickets about slowness: down 70%

The migration was painful but worth it. If you’re still on CRA, start with your public pages — the wins are fast and build the team’s confidence for the harder dashboard work ahead.