If you search for "Next.js App Router migration," you will find dozens of perfectly polished tutorials. They show you how to move a simple blog from the old pages directory to the new app directory in 15 minutes.
I am here to talk about what happens when you try to migrate a messy, real-world, highly interactive SaaS application with thousands of active users.
Spoiler: It took us three weeks, broke our authentication flow twice, and completely shattered how I thought about web development. But we made it out the other side, and the performance gains are actually real.
If you are planning this migration, here are the hidden traps you need to watch out for.
The "use client" Contagion
When you first read about React Server Components (RSC), the pitch sounds amazing. Send zero JavaScript to the client! Fetch data right on the server!
So, I started moving our dashboard over. I created a server component, fetched the user data, and passed it down to a child component. But wait, that child component has a dropdown menu that needs useState. So, I added "use client" to the top of that file.
Suddenly, I realized that the dropdown imported a context provider, which imported our theme configuration, which imported half of our component library. Before I knew it, a single "use client" directive at the top of a file had cascaded down, turning 80% of my fancy new Server Components back into Client Components.
The fix: You have to completely rewire your brain to push interactivity to the absolute edges of your component tree. We spent days refactoring our UI just to isolate the stateful parts into tiny, wrapping components so the parent layouts could stay on the server.
The Caching Will Drive You Crazy
Next.js App Router caches everything by default. I mean everything.
I deployed our staging environment and noticed that when a user updated their profile picture, the old picture kept showing up. I refreshed the page. Old picture. I hard-refreshed. Old picture.
I went down a massive rabbit hole trying to understand the difference between the Data Cache, the Full Route Cache, the Router Cache, and the unholy mess of revalidateTag vs revalidatePath.
The documentation explains how these work in isolation, but debugging them in a live environment is brutal.
The fix: Start dumb. We ended up explicitly opting out of caching on almost all of our authenticated routes by using export const dynamic = 'force-dynamic' at the top of our page files. Once the app was stable and working predictably, we slowly started layering the cache back in where it actually mattered (like public-facing marketing pages).
Third-Party Libraries Aren't Ready
This was the biggest roadblock. We rely heavily on a popular drag-and-drop library and an analytics wrapper. Both of them immediately threw errors about window being undefined the second they touched a Server Component.
You will spend a significant amount of your migration writing wrapper components just to make old libraries play nice with the new paradigm.
Was it worth it?
Honestly, week two of the migration was miserable. I was ready to roll back to the pages directory and never look at Server Components again.
But now that we are on the other side? Our initial page load times dropped by 40%. Our bundle size is a fraction of what it used to be. Writing async server components that just hit the database directly without needing to set up a separate API route feels like a superpower.
If you are about to start this migration: don't do it in one giant pull request. Move your static pages first, wrap your providers carefully, and accept that you are going to spend a lot of time fighting the cache.
Has anyone else survived this migration yet, or are you all staying on the pages router until the end of time?
Top comments (1)
The profile picture surviving even a hard refresh is the part I'd turn into a migration acceptance test. After the two authentication breakages, I'd also exercise sign-in, profile edits, navigation away and back, and sign-out as one complete journey before moving the next route. That gives the incremental rollout a useful definition of "done": the 40% faster initial load is a meaningful gain, but users still need to trust what the dashboard shows after they change something.