We'd been running a custom React SPA with client-side rendering since 2020. It worked, but our Core Web Vitals were embarrassing: LCP of 4.1 seconds on mobile, a CLS that made the homepage look like it was having a seizure on first load, and an SEO situation that required considerable creativity to work around the client-rendering limitations.

We rebuilt the site in Next.js 14 App Router over two months and deployed in August. Here's the honest six-month report.

The Performance Numbers (Real Data)

Before: LCP 4.1s (mobile), CLS 0.18, FID 140ms.
After three months of optimisation: LCP 1.4s (mobile), CLS 0.04, INP 62ms.

These are not marketing numbers — they're from our Google Search Console and Lighthouse CI runs. The LCP improvement is the one that materially affected our organic search. We saw a 23% increase in organic traffic over the three months following the migration, which isn't entirely attributable to the performance change (we also improved content), but Core Web Vitals being a ranking signal is not theoretical — it moved.

The App Router: The Good Parts

Server Components are genuinely great for data fetching once you understand the mental model. Being able to write a component that fetches its own data on the server, serialises it to the client, and doesn't ship any fetch logic or API client in the browser bundle is a real improvement over the Redux + useEffect patterns we were using before.

Nested layouts are also excellent. Our main marketing pages, our blog, and our app all have different layout requirements. With the App Router, each section has its own layout file and the router handles them correctly without any manual switching logic.

The App Router: The Frustrating Parts

The mental model for caching is confusing. There are at least four different caching layers in Next.js 14 (fetch cache, router cache, data cache, full-route cache) and understanding how they interact takes time and periodic re-reading of the docs. I've had bugs caused by cache mismatches that took embarrassingly long to diagnose.

The "use client" vs server component boundary is conceptually correct but practically annoying. Context, state, browser APIs, most third-party UI libraries — all of these require client components. Learning to think in terms of which parts of your tree need to be interactive, and designing your component boundaries accordingly, is a genuine mental shift that the tutorials undersell.

Migration Cost

We estimated four weeks. It took nine. The extra time went to: rewriting all our data fetching to use server components, dealing with third-party libraries that weren't compatible with the App Router, debugging deployment issues on Vercel that we'd never seen in development, and the caching surprises mentioned above. Budget for longer than you think.

Would I Do It Again?

Yes, but with better timeline estimates. The performance and developer experience improvements are real and compounding. Once you're comfortable with the mental model, writing features in Next.js 14 is genuinely faster than in our old setup. The migration cost was real but one-time; the benefits are ongoing.