Skip to main content
Back to BlogFull-Stack Development

Next.js App Router Performance: A Practical Optimisation Guide

iSpecia Engineering Team March 18, 2025 12 min read
Next.js App Router Performance: A Practical Optimisation Guide

The App Router unlocks powerful caching and rendering patterns — but only if you understand how they interact and where the pitfalls are.

Why the App Router Changes the Performance Conversation

Next.js 13+ App Router is a genuinely different architecture from the Pages Router. React Server Components (RSC), streaming, nested layouts, and a new caching model give you more control over performance than ever before — but they also introduce new failure modes that are less obvious than classic client-side performance problems.

The biggest shift: data fetching is now co-located with components at the server level. There is no getServerSideProps or getStaticProps. Components fetch their own data, and the React runtime orchestrates streaming and suspension. This is powerful but requires a clear mental model of what runs on the server, what is cached, and what triggers re-renders.

Understand the Caching Layers Before Anything Else

Next.js App Router has four distinct caching layers: Request Memoization (deduplicates identical fetch() calls within a single request), Data Cache (persists fetch() results across requests, similar to ISR), Full Route Cache (caches rendered RSC output at build time or on first request), and Router Cache (client-side cache of visited route segments). Each has different invalidation semantics.

The most common source of confusing behaviour: a fetch() that you expect to be fresh is actually being served from the Data Cache. By default, fetch() calls in Server Components are cached indefinitely. Use { cache: 'no-store' } for data that must be fresh on every request, and { next: { revalidate: 60 } } for data that can be stale for up to 60 seconds. Getting this right has a larger performance impact than most other optimisations.

Minimise the Client Bundle Aggressively

Every component that has 'use client' at the top becomes part of the JavaScript bundle shipped to the browser. In the Pages Router, everything was client JavaScript. In the App Router, the default is server — which means you should be deliberate about what you mark as client-only.

A common mistake: marking a parent layout as 'use client' because it needs one interactive element, which forces all its children into the client bundle too. Instead, push interactivity down to leaf components. A search input, a dropdown, a modal trigger — these should be isolated client components. The surrounding layout, navigation, and content can remain server components.

Streaming and Suspense for Perceived Performance

The App Router's streaming support lets you progressively render a page as data becomes available. Wrap slow data-fetching components in a Suspense boundary with a loading skeleton, and the rest of the page renders immediately while the slow part catches up. This dramatically improves perceived load time even when the total data fetching time is the same.

Design your Suspense boundaries carefully. A single Suspense boundary that wraps an entire page means the user sees nothing until the slowest query completes. Multiple fine-grained boundaries mean the user sees content progressively. For pages with heterogeneous data fetching speeds, fine-grained Suspense is almost always the right choice.

Image Optimisation: Still the Biggest Win

The next/image component remains one of the highest-leverage optimisations in Next.js. It automatically converts images to WebP/AVIF, serves responsive sizes via srcset, and lazy-loads below-the-fold images. But it only works well if you set the sizes prop correctly — without it, Next.js cannot generate the right srcset and will serve larger images than necessary.

For above-the-fold hero images, add the priority prop to preload them. For layout-shifting images, always provide explicit width and height (or use fill with a sized container). Unoptimised images are consistently in the top three causes of poor Core Web Vitals scores in Next.js apps we audit.

Profiling Before Optimising

Before spending a sprint on performance optimisation, measure first. The Next.js built-in analytics (available on Vercel) and the React DevTools Profiler both show component render times. Lighthouse and WebPageTest give you field-data-representative metrics. Chrome DevTools Performance tab shows the full JavaScript execution breakdown.

In our experience auditing Next.js apps, the performance bottlenecks are almost never where developers assume. Slow API routes are more often a database query problem than an application logic problem. Large bundles are more often an unanalysed import than a framework issue. Measure before you optimise.

Next.jsPerformanceApp RouterReactWeb Dev

Work With Us

Ready to put this into practice?

iSpecia builds what you've been reading about. Tell us your challenge.