Web Development

Next.js 14 App Router: The Performance Patterns That Actually Matter

Server Components, Partial Prerendering, and streaming are powerful — but easy to misuse. Here are the patterns we use to hit 95+ Lighthouse scores consistently.

By Ravi Tripathi, CTO & Co-FounderApril 7, 20269 min read
Next.js 14 App Router: The Performance Patterns That Actually Matter

React Server Components Change Everything

The Next.js App Router is not just a routing upgrade — it is a fundamental shift in how you think about data fetching and rendering. After rebuilding five production applications on App Router in 2024, here is our opinionated take on what the performance patterns are.

Pattern 1: Fetch as Close to the Data as Possible

With RSC you can fetch data directly inside a Server Component without an API round-trip. Fetch at the leaf level, not the root. This enables component-level caching and means a cache miss for one component does not invalidate the entire page.

// ✅ Good — leaf-level fetch with component-scoped cache
async function UserCard({ userId }: { userId: string }) {
  const user = await fetch(`/api/users/${userId}`, { 
    next: { revalidate: 60 } 
  }).then(r => r.json());
  return <div>{user.name}</div>;
}

Pattern 2: Parallel Data Fetching with Promise.all

Sequential awaits in a Server Component create a waterfall. Use Promise.all to fetch independent data sources in parallel.

Pattern 3: Streaming with Suspense for Perceived Performance

Wrap slow data-dependent components in <Suspense> with a skeleton fallback. The page shell renders instantly (great for LCP) and the slow sections stream in progressively. This is the single highest-impact change we make when migrating apps to App Router.

Pattern 4: Partial Prerendering (PPR) for Mixed Pages

PPR (experimental in Next.js 14, stable in 15) lets you statically prerender the page shell at build time while streaming dynamic holes at request time — combining the best of SSG and SSR. Enable it per-route with export const experimental_ppr = true.

Pattern 5: Avoid "use client" at the Top of Trees

The most common App Router performance anti-pattern: marking a parent layout as a Client Component because one child needs interactivity. Instead, push the "use client" boundary down to only the interactive component. Keep the surrounding tree as Server Components.

Results

Applying these patterns when migrating a SaaS dashboard (previously Pages Router) to App Router: LCP improved from 3.2s to 0.8s, TBT from 420ms to 60ms, Lighthouse performance score from 72 to 97.

Frequently Asked Questions

Should I migrate from Pages Router to App Router now?+

For new projects, absolutely yes. For existing apps, migrate incrementally — Next.js fully supports both routers in the same project. Prioritise migrating pages that have complex data-fetching or poor Lighthouse scores first.

Does using Server Components break my existing React libraries?+

Libraries that use hooks, context, or browser APIs need to be in Client Components. Most UI libraries (Radix, shadcn/ui, Headless UI) are already App Router compatible. Check the library's README for the "use client" compatibility note.