CODOHUB
ENGINEERING 12 MIN READ

Next.js 15 Performance Optimization Guide

2026.01.28 12 MIN
Next.jsperformanceReact

Next.js 15 introduced significant improvements to the React Server Components model, the App Router, and the bundler architecture. If you are building production applications and are not taking advantage of these features, you are leaving substantial performance on the table. This guide covers the most impactful optimisations you can apply today.

01

Server Components: The Biggest Performance Win

React Server Components (RSC) allow you to render components on the server without sending any JavaScript to the client. For data-heavy pages — dashboards, product listings, blog archives — RSC can reduce JavaScript bundle sizes by 60% or more. The rule is simple: if a component does not need interactivity or browser APIs, make it a Server Component.

In Next.js 15, Server Components are the default. You only opt into client rendering by adding 'use client' at the top of a file. Audit your codebase and push that directive as far down the component tree as possible. This single change has the biggest impact on Time to Interactive (TTI) and Largest Contentful Paint (LCP).

To make this concrete: imagine a product listing page that fetches 50 products from a database, renders a filter sidebar, and shows a promotional banner. With the Pages Router approach, all of this would ship as a JavaScript bundle the browser must download, parse, and hydrate before the user can interact. With Server Components in the App Router, the product list and banner render as static HTML on the server — zero JS sent for those components. Only the interactive filter sidebar (where the user can change category or price range) ships as client JavaScript. The practical result on a real e-commerce project: bundle size dropped from 380kb to 140kb, and Largest Contentful Paint improved from 2.8 seconds to 1.1 seconds.

The migration path from Pages Router is incremental. You do not need to rewrite your entire app at once. Start by converting leaf-node components — ones that receive props but have no state or event handlers — to Server Components first. Work your way up the tree, extracting only the interactive islands into 'use client' boundaries.

02

Streaming with Suspense for Faster Perceived Performance

Next.js 15 streaming via React Suspense allows the server to send the page shell immediately and stream slower data-dependent sections as they resolve. Wrap expensive data-fetching components in <Suspense> with a meaningful skeleton fallback. Users see content faster, even before the most expensive queries complete.

This is especially powerful for pages with mixed content: static hero sections load instantly, while personalised or database-driven sections stream in. When implemented correctly, perceived load time drops dramatically even when actual data-fetch time stays the same.

Here is the practical pattern. In your page component, fetch non-critical data inside a separate async Server Component and wrap it in Suspense. The shell — navigation, hero, static content — renders and reaches the user in milliseconds. The slower component (say, a personalised recommendation section that requires a database query joining user history with product inventory) streams in as soon as its data resolves, replacing the skeleton. The user perceives the page as having loaded instantly, even though full data took 800ms.

A critical mistake to avoid: do not waterfall your Suspense boundaries. If Component B depends on data from Component A, they must share a parent fetch or use Promise.all — not nested awaits. Waterfalls eliminate the streaming benefit entirely. Use React's cache() utility to deduplicate fetch calls across the component tree when multiple components need the same data.

03

Caching Strategy in the App Router

Next.js 15 offers four caching layers: Request Memoisation, Data Cache, Full Route Cache, and Router Cache. Understanding when each layer applies — and when to opt out — is critical for building apps that are both fast and fresh.

For mostly-static content, use the default full route cache. For real-time data (prices, stock levels, live scores), use { cache: 'no-store' } or revalidate on a short interval. For query-heavy API routes, use unstable_cache with appropriate tags so you can purge specific cache entries on mutation without a full rebuild.

The most powerful pattern in Next.js 15's caching model is tag-based revalidation. Instead of setting a time-based revalidation interval (which causes stale data to persist up to the interval duration), tag your fetch calls with entity identifiers and call revalidateTag() on mutation. For example, tag all product page fetches with ['product', `product-${id}`]. When a product is updated via your admin dashboard, call revalidateTag(`product-${id}`) and only that product's cached data is purged — not the entire cache. This lets you serve fully cached pages most of the time while guaranteeing fresh data appears within milliseconds of a content update.

Note that in Next.js 15, fetch() caching behaviour changed from Next.js 14: fetch is no longer cached by default. You must explicitly opt into caching with { next: { revalidate: seconds } } or { next: { tags: ['...'] } }. Audit your data fetching code when upgrading from 14 to 15 to avoid accidentally serving uncached responses where you expect cached ones.

04

Image and Font Optimisation

Always use next/image with explicit width and height — this prevents layout shift and enables automatic WebP/AVIF conversion. Use the priority prop on above-the-fold images to trigger early preloading. For fonts, use next/font/google with display: 'swap' and preload: true. Self-hosting fonts removes a cross-origin DNS lookup from your critical path.

Beyond these basics, several additional image optimisation patterns have measurable impact on Core Web Vitals. For hero images, use a srcSet with multiple sizes and let the browser choose the most appropriate resolution — next/image handles this automatically, but ensure your sizes prop accurately describes the rendered width at different breakpoints. An image that renders at 400px wide on mobile should not download a 1200px asset.

For galleries and image-heavy pages, implement blur placeholder (blurDataURL) to prevent layout shift during progressive loading. Generate a low-resolution base64 placeholder at build time using a tool like plaiceholder and pass it as the placeholder prop. The result is a smooth blur-to-sharp transition that eliminates Cumulative Layout Shift (CLS) caused by images loading after surrounding text.

For third-party images (user avatars, CMS-served media), add their domains to the remotePatterns config in next.config.js rather than using unoptimised={true}. This ensures next/image's optimisation pipeline still processes external images rather than serving them raw.

05

Bundle Analysis and Dead Code Elimination

Even with Server Components reducing your client bundle, it is worth periodically analysing what remains. Next.js ships a built-in bundle analyser via the @next/bundle-analyzer package. Add it to your next.config.js, run ANALYZE=true next build, and review the treemap output to identify unexpectedly large dependencies.

Common culprits in Next.js apps include: date libraries (moment.js weighs 300kb+ minified — replace with date-fns or the native Intl API), icon libraries imported incorrectly (importing all icons instead of individual ones), and heavy chart libraries loaded on pages where they are only used conditionally. For any heavy library that is only needed on specific pages or behind user interaction, use dynamic imports with { ssr: false } to split it into a separate chunk that loads on demand.

Another impactful optimisation is barrel file elimination. Files like index.ts that re-export from dozens of modules force the bundler to pull in the entire module graph even when only one export is needed. In Next.js 15, use the optimizePackageImports config option to tell the bundler which packages should be tree-shaken at the import level rather than the barrel level. This alone can reduce client bundle sizes by 15-30% in component-library-heavy codebases.

— Conclusion

Performance is a feature, not an afterthought. Implementing these optimisations in Next.js 15 will measurably improve your Core Web Vitals scores, SEO rankings, and conversion rates. CodoHub builds all client websites with these patterns baked in from day one. Contact us if you'd like a performance audit of your existing Next.js application.

Next.js performance React web development Core Web Vitals

Codohub — Software Development Agency

TURN THIS INSIGHT
INTO ACTION

Let Codohub build your next digital product — fast, scalable, and built to convert.