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.