Most migration retrospectives are written by people who just finished the migration. They’re optimistic, they’re still close to the problems they solved, and the new issues haven’t shown up yet.

This is a different kind of post. It’s based on conversations with engineering leads whose teams migrated to the Next.js App Router 6-12 months ago - people who are now living with the decision, shipping features on it, and have seen where it helped and where it didn’t.

The verdict is nuanced. The App Router is genuinely better in ways that matter. The migration and initial learning curve were harder than most teams expected. And some rough edges are still there.

What Got Better

Performance on Initial Load

The original pitch for Server Components was faster initial load times because JavaScript isn’t sent to the client for server components. In practice, this delivers. Teams report 15-30% improvements in First Contentful Paint (FCP) and Largest Contentful Paint (LCP) for pages that were data-heavy.

The mechanism is simple: instead of useEffect(() => { fetch('/api/user') }), you write const user = await db.users.findById(userId) in a Server Component. The data arrives with the HTML, before the client even loads JavaScript. No loading spinners for above-the-fold content.

For e-commerce, marketing pages, and dashboards with significant initial data requirements, this is a real user experience improvement.

Colocated Data Fetching

The Pages Router forced all data fetching into getServerSideProps or getStaticProps at the page level. A deep component that needed data had to receive it as props passed down from the page, or use a client-side fetch with a loading state.

App Router lets any component fetch its own data:

// This component fetches its own data directly
async function UserAvatar({ userId }: { userId: string }) {
  const user = await db.users.findById(userId);
  return <img src={user.avatarUrl} alt={user.name} />;
}

The colocation improvement is real. Components are self-contained. You don’t have page-level files that assemble data from 12 different sources and thread it down to leaf components.

Streaming and Suspense

Streaming + Suspense is underappreciated. With the App Router, you can wrap slow components in <Suspense> and show a fallback while the server-side data fetch completes. The page delivers the fast parts immediately and fills in slow parts as they complete.

<Suspense fallback={<RecentOrdersSkeleton />}>
  <RecentOrders userId={user.id} />  {/* slow db query */}
</Suspense>

Users see content faster. The server sends HTML progressively. This is a meaningful UX improvement for pages with mixed-speed data sources.

What Got Harder

The Caching Model

This is the honest pain point. Next.js 14 had aggressive default caching that surprised nearly every team. Fetches were cached by default, and invalidation required explicit configuration that wasn’t obvious from the documentation.

Teams would deploy updates and wonder why old data appeared. The answer was often in the caching layer - Route Cache, Data Cache, or Request Memoization - and figuring out which layer was caching incorrectly and how to invalidate it cost many engineering hours.

Next.js 15 changed the defaults (dynamic by default instead of cached by default) and improved the documentation significantly. But teams that migrated during the 14.x window still have scar tissue from caching issues that shouldn’t have required this level of understanding.

The Server/Client Mental Model

Every engineer on the team needs to internalize the server/client component distinction. This is genuinely new knowledge - it doesn’t map cleanly to any previous React concept.

Common mistakes that appear in code review:

  • Importing a server-only module in a client component (breaks the build)
  • Trying to use useContext in a Server Component (doesn’t work)
  • Passing non-serializable props from a Server Component to a Client Component
  • Using useState in a file marked as a Server Component

The TypeScript errors for these are getting better, but the mental model tax is real. Senior engineers who have been doing React for 5+ years need to update their instincts. Junior engineers need to learn the boundary from day one.

Third-Party Library Compatibility

Some libraries don’t work in Server Components or have issues with the App Router build process. The commonly-cited ones: some animation libraries, some state management libraries that rely on render-level effects, and some older component libraries that haven’t been updated for React 19.

Most issues have workarounds (wrapping in a client component, adding 'use client' at the right level), but every workaround represents investigation time.

The Migration Experience

Teams that migrated from Pages Router report the following timeline buckets:

Migration Type Estimated Timeline
Small app (< 20 routes) 2-3 weeks
Medium app (20-100 routes) 1-3 months
Large app (100+ routes) 3-6 months
App with heavy CSS-in-JS Add 2-4 weeks

The most time-consuming part: not converting page components, but hunting down the cases where your Pages Router assumptions don’t hold in App Router. The useRouter API changed. The way you access route params changed. The way you do redirects in middleware changed. Each of these is documented but finding all instances takes longer than expected.

What Teams Would Do Differently

From conversations with teams post-migration, common learnings:

  • Migrate route by route, not all at once. Next.js supports Pages and App Router coexisting.
  • Get the caching model right on the first few routes before proceeding.
  • Invest in a team knowledge session on Server vs. Client components before starting.
  • Run the App Router experiment in a feature branch for 2-3 months before committing.

Was It Worth It?

For performance-conscious applications (e-commerce, content sites, dashboards): yes. The LCP improvements, streaming, and colocated data fetching make the application objectively better for users.

For smaller apps that weren’t performance-constrained: debatable. The developer experience improvements are real but the migration cost may not pay back on a 3-5 person team with a solid Pages Router application.

For new projects: the App Router is the right default. Don’t start with Pages Router in 2026.

Bottom Line

The App Router is better than the Pages Router. The performance benefits are real, the developer experience improvements are genuine, and React Server Components are the right abstraction for data-heavy applications. The migration cost was higher than advertised due to the caching model confusion and the ecosystem compatibility issues. Six months out, teams are generally happy they made the switch and would not go back - but they wouldn’t recommend the migration timeline their vendors suggested.