Next.js App Router: Brutal Lessons Learned in Production
When Next.js launched the App Router and React Server Components (RSC), the hype was deafening. "Zero client-side JavaScript!" "Native data fetching!" We bought into the hype and decided to migrate our mid-sized SaaS dashboard. It was a humbling experience.
The Caching Nightmare
In the old Pages router, caching was explicitly controlled via getServerSideProps or getStaticProps. In the App router, Next.js decided to aggressively cache everything by default using a heavily patched fetch API.
We deployed our first iteration and immediately got bug reports: users were updating their profile settings, navigating away, and seeing old data when they returned. The aggressive client-side router cache and server-side fetch cache were conflicting. We spent days wrestling with revalidatePath and revalidateTag just to get basic CRUD operations to reflect instantly.
Lesson: Opt out of caching entirely for dynamic routes during your initial build (export const dynamic = "force-dynamic"), and then surgically add caching back only where it's strictly necessary for performance.
Client vs Server Components Boundary
The mental model shift is severe. You cannot pass functions or non-serializable data from a Server Component down to a Client Component. We had massive complex context providers wrapping our app that suddenly broke because they were trying to run on the server.
We had to re-architect our component tree, pushing interactivity ("use client") as far down the tree as possible. Instead of making an entire dashboard a client component, we isolated just the "DropdownButton" and the "Chart" as client components, leaving the layout and data-fetching at the server level.
Was it worth it?
Eventually, yes. Once we tamed the cache and understood the boundaries, the amount of JavaScript shipped to the browser dropped by 40%. The initial page load is blazing fast. But if you are planning a migration, triple your timeline estimates. It's not an upgrade; it's a paradigm shift.