Next.js App Router: 6 Months In
July 12, 2025 · 10 min read
We Did the Migration. Here's the Honest Take.
Six months ago we migrated a 40k-line Next.js codebase from Pages Router to App Router. This is not a tutorial. It's a retrospective — what's genuinely better, what burned us, and what we'd do differently.
What's Actually Better
Layouts That Don't Re-Render
The single biggest win. With Pages Router, layouts re-rendered on every navigation. With App Router, nested layouts persist. No more flash of unstyled sidebar. No more scroll position resets.
Colocation
Data fetching lives next to the component that uses it. No more hunting through getServerSideProps then component then prop drilling chain. Server Components changed how I think about data flow.
Streaming
Complex pages that previously blocked on slow data fetches now stream. Wrap the slow part in Suspense and ship a skeleton instantly. Users perceive the app as faster even when the data takes the same time.
The Footguns
"use client" Boundary Confusion
This was the biggest source of bugs. Developers would add "use client" to a component and not realize it opted out the entire subtree from server rendering. We now have a lint rule: any "use client" addition requires a code review comment explaining why.
Caching Behavior
The App Router's default caching is aggressive. Fetches are cached by default. Cookies aren't included by default. We spent two weeks debugging why authenticated data was being served to the wrong users before we understood the caching semantics fully.
// Cached by default — probably wrong for auth data
const data = await fetch("/api/user")
// Opt out of caching
const data = await fetch("/api/user", { cache: "no-store" })Third-Party Libraries
A meaningful percentage of popular React libraries assume Client Components. Some don't work in Server Components at all. Audit your dependencies before migrating — not after.
What We'd Do Differently
Migrate incrementally. App Router and Pages Router can coexist in the same project. We tried to do it all at once, which created a 3-week period where the app was partially broken. Route by route is slower but saner.
Write migration tests first. For each route, document its current behavior (response shape, auth requirements, caching semantics) before touching it. Run those assertions post-migration.
Bottom Line
App Router is the right direction. The mental model is better. The performance primitives are better. The developer experience — once you understand the caching model — is better.
But it's a paradigm shift, not an upgrade. Budget accordingly.
← Back to Blog