Concrete performance wins from migrating to App Router. Server Components, streaming, parallel data fetching—real metrics from my portfolio and Hoop Almanac, not theoretical benefits.
Everyone says App Router is faster. But what actually improves? I rebuilt my portfolio with App Router and optimized Hoop Almanac. Here's what moved the metrics.
In Pages Router, everything ships to the client. In App Router, Server Components stay on the server. My portfolio's project listing page went from 89KB to 34KB JavaScript. The data fetching, formatting, and sorting all happen server-side now.
The pattern: anything that doesn't need interactivity is a Server Component. The project cards themselves are Server Components. Only the filter buttons and search input are Client Components.
Hoop Almanac's player dashboard loads multiple data sources: player stats, predictions, news, trade suggestions. In Pages Router, I'd fetch all data before rendering. Users stared at a spinner.
With App Router, each section is wrapped in Suspense. The page shell renders immediately. Stats load first (fastest query). Predictions stream in next. News and trades load last. Users see progress instead of waiting.
Time to First Byte stayed the same, but perceived performance improved dramatically. Users could start reading player stats while predictions loaded.
Pages Router's getServerSideProps ran sequentially by default. If you needed three API calls, you'd often await them one after another. App Router encourages parallel fetching.
In my portfolio, the landing page fetches profile, projects, and testimonials. Three separate Server Components, three parallel fetches. Combined latency dropped from ~800ms to ~300ms (limited by the slowest query, not the sum).
Slapping 'use client' on everything defeats the purpose. In Hoop Almanac, I initially made the entire draft board a Client Component (it needs real-time updates). The bundle bloated back up.
The fix: composition. The draft board LAYOUT is a Server Component. Only the interactive pieces (pick button, timer, chat) are Client Components. The player list renders server-side, the selection handler is client-side.
App Router caches fetch() by default. Great for static data, confusing for dynamic data. In OpportunIQ, diagnostic results were showing stale data because the AI response was cached.
The fix: explicit cache control. Use { cache: 'no-store' } for dynamic data, or revalidate tags for ISR-like behavior. Don't assume—be explicit about caching strategy for each fetch.
Before (Pages Router): LCP 2.4s, FCP 1.8s, JS Bundle 156KB. After (App Router): LCP 1.1s, FCP 0.9s, JS Bundle 67KB. Lighthouse Performance: 72 → 94.
The gains came from: Server Components (smaller bundle), parallel fetching (faster data), image optimization with next/image priority hints, and removing unnecessary client-side state.
1) Start with leaf components. Convert static UI to Server Components first. 2) Move data fetching up. Fetch in Server Components, pass data down. 3) Add 'use client' only when needed—interactivity, hooks, browser APIs. 4) Wrap slow sections in Suspense for streaming. 5) Audit your fetches—remove default caching where inappropriate.
App Router's performance benefits are real but not automatic. You get them by thinking about server vs client boundaries, parallelizing fetches, and streaming content. The framework enables performance—you still have to architect for it.