Introduction
We've been running Next.js App Router in production across 15+ client projects since its stable release. After navigating breaking changes, caching quirks, and performance cliffs, we've developed a set of patterns that consistently deliver fast, reliable applications.
This post covers the advanced techniques we use for applications serving 100K+ daily users.
Caching Strategies
The App Router's caching is powerful but nuanced. Our caching playbook:
- Static generation by default: Use
generateStaticParamsfor any page with a finite set of paths - ISR for dynamic content:
revalidate: 60gives you near-real-time data with static performance - Route segment config: Set
dynamic = 'force-static'on marketing pages,dynamic = 'force-dynamic'on dashboards - Unstable cache (now stable): Use
unstable_cachefor database queries with tag-based revalidation - Client-side caching: SWR or React Query for data that changes after initial load
The key insight: think about caching at the segment level, not the page level.
Streaming SSR
Streaming is the App Router's killer feature for performance:
- Suspense boundaries: Wrap each data-fetching section in its own Suspense boundary
- Loading UI: Always provide meaningful loading states — skeleton screens, not spinners
- Progressive rendering: Structure your page so the most important content renders first
- Parallel data fetching: Use Promise.all for independent data sources within a single component
We've seen Time to First Byte (TTFB) improvements of 40-60% by adopting streaming SSR with well-placed Suspense boundaries.
Parallel Routes
Parallel routes are underused but powerful for complex UIs:
- Dashboard layouts: Render multiple independent panels that load independently
- Modal routes: Implement modals as parallel routes for proper URL handling and back-button behavior
- Conditional rendering: Show different content based on authentication state without layout shifts
- Error isolation: A failure in one parallel route doesn't crash the entire page
The pattern: create named slots (@panel, @modal, @sidebar) in your layout and let each slot manage its own loading, error, and content states.
Bundle Optimization
Keeping bundles small in a large App Router application:
- Dynamic imports: Use
next/dynamicfor heavy components (charts, editors, maps) - Route groups: Organize routes to share layouts efficiently without unnecessary re-renders
- Server Components by default: Only add 'use client' when you need interactivity
- Package analysis: Run
@next/bundle-analyzermonthly and remove unused dependencies - Font optimization: Use
next/fontto eliminate font-related layout shift
Our target: under 100KB first-load JS for content pages, under 200KB for interactive dashboards.
Performance Monitoring
You can't optimize what you don't measure:
- Core Web Vitals: Monitor LCP, CLS, and INP in production with real user data
- Custom metrics: Track time-to-interactive for your most important user flows
- Error tracking: Monitor hydration errors — they're silent performance killers
- Build analytics: Track build times and bundle sizes as part of your CI pipeline
We use a combination of Vercel Analytics, custom New Relic dashboards, and Lighthouse CI to maintain performance budgets across all our Next.js projects.
Written by
Marcus Johnson
Senior Frontend Architect
Part of the Fixl engineering team, sharing insights from building production-grade software for startups and enterprises.