A deep dive into structuring large-scale Next.js applications with the App Router — layouts, server components, caching, and performance optimization.
The App Router changed how we think about building Next.js applications. Server Components, nested layouts, and built-in caching give us a lot of power — but also a lot of new decisions to make. Here's what we've learned shipping production apps with it.
1. Default to Server Components
Every component is a Server Component unless you add "use client". Keep it that way as long as possible. Push client components to the leaves of your tree — a button, a form, an interactive widget — rather than wrapping entire pages in "use client". This keeps your JavaScript bundle small and your initial page load fast.
2. Co-locate data fetching with the component that needs it
With the App Router, you can fetch data directly inside Server Components using async/await. Next.js automatically deduplicates identical fetch requests across a render tree, so don't be afraid to fetch the same resource in multiple components — it's cheaper than prop-drilling.
3. Use layouts for shared UI, not shared state
Layouts are great for navigation, footers, and shells that persist across route changes. But layouts don't re-render on navigation between sibling routes, so don't rely on them for state that needs to reset. Use route groups to organize related routes without affecting the URL structure.
4. Get caching right early
- Static data: let Next.js cache fetches by default for content that rarely changes.
- Dynamic data: opt out with { cache: 'no-store' } or revalidate frequently with { next: { revalidate: 60 } }.
- Use generateStaticParams for dynamic routes you can pre-render at build time.
- Lean on the Next.js cache debugging headers in development to see what's actually being cached.
5. Handle loading and error states with conventions, not props
loading.tsx and error.tsx files give you automatic Suspense boundaries and error boundaries per route segment — no extra wiring required. Use them generously; users perceive an app with good skeleton states as significantly faster, even when total load time is identical.
The App Router has a learning curve, but the mental model — server-first, client where needed, caching as a first-class concern — pays off enormously at scale. Start small, measure your bundle size, and let the framework do the heavy lifting.
