Development

Next.js Performance: A Practical Guide for Next.js 16

What actually makes a Next.js site fast: Core Web Vitals, Server Components, images, fonts, Cache Components and what to check when upgrading from Next.js 15.

Vishal Shah

Updated 5 min read

Speed is not just a technical score. In the "Milliseconds Make Millions" study by Deloitte and Google, which covered 37 brands and 30 million sessions, a 0.1-second improvement in mobile site speed was linked to an 8.4% rise in retail conversions and a 9.2% rise in average order value.

Next.js gives you most of the tools to build a fast site, but it does not make a site fast by default. This guide covers what matters most in Next.js 16, including fixes we made on this website, and what to watch if you are upgrading from Next.js 15.

Measure the right things first

Google's Core Web Vitals are the most useful starting point, because they measure what users feel and they feed into search ranking:

  • Largest Contentful Paint (LCP): how quickly the main content appears. Aim for 2.5 seconds or less.
  • Interaction to Next Paint (INP): how quickly the page responds to taps and clicks. Aim for 200 milliseconds or less.
  • Cumulative Layout Shift (CLS): how much the page jumps around while loading. Aim for 0.1 or less.

Google judges these at the 75th percentile of real visits, so your slower users count. Lighthouse and PageSpeed Insights lab scores are useful for finding problems, but the field data in Search Console tells you whether real visitors are having a good experience. Fix what the field data shows first.

Keep JavaScript on the server

In the App Router, components are Server Components unless you mark them otherwise. They render to HTML on the server and send no component JavaScript to the browser. The most common mistake we see is putting "use client" at the top of a page or layout, which pulls everything below it into the browser bundle.

Push client components down to the leaves instead. A product page can be a Server Component that renders the title, description and images, with only the "add to basket" button and the image carousel as client components. Less JavaScript means the main thread is free sooner, which helps INP on mid-range phones.

Third-party scripts often cost more than your own code. Analytics, chat widgets and heatmap tools all compete for the same main thread. Load them with next/script using the lazyOnload strategy where you can. On this site, analytics only loads after a visitor accepts cookies, which is also what UK consent rules require.

Get images right

Images are usually the biggest files on a page and often the LCP element. next/image resizes and converts them for you, but only if you tell it how big the image will be on screen.

When we audited this website, we found images using fill without a sizes value. Without sizes, the browser assumes the image is the full width of the screen and downloads a far larger file than it needs. Some of our team photos were PNG files of 1.7 to 2.3 MB. The fixes were simple:

  • Add an accurate sizes value to every image that uses fill or responsive widths.
  • Turn on AVIF output with images.formats: ['image/avif', 'image/webp'] in next.config. AVIF files are usually smaller than WebP at the same quality.
  • For decorative images hidden on mobile, use a sizes value that gives phones a tiny variant, so they do not download an image they never show.
  • Give the LCP image, usually the hero, fetchPriority="high" so it starts loading straight away, and leave it off everything else. The older priority prop is deprecated in Next.js 16.

Always give images a fixed size or aspect ratio so the layout does not jump when they load. That is the quickest CLS win on most sites.

Load fonts without the flash

Web fonts can delay text and shift the layout when they swap in. Self-host your fonts rather than loading them from a third-party server, preload the one or two files used above the fold, and use font-display: swap with a fallback of similar size. next/font handles self-hosting and preloading for you. Where it does not fit your setup, a plain @font-face rule plus a preload link works just as well. That is what we use on this site for our heading font.

Cache deliberately with Cache Components

Next.js 16 introduced Cache Components, which make caching explicit. You turn it on with cacheComponents: true in next.config. Code is dynamic by default, and you opt in to caching with the "use cache" directive on a page, component or function:

import { cacheLife, cacheTag } from 'next/cache';

export async function getProducts() {
  'use cache';
  cacheLife('hours');
  cacheTag('products');

  const res = await fetch('https://api.example.com/products');
  return res.json();
}

cacheLife sets how long the result stays fresh, using presets such as 'minutes', 'hours' and 'days'. cacheTag labels the data so you can refresh it when it changes:

  • revalidateTag('products', 'max') marks the data as stale. Visitors keep getting the cached version while a fresh copy is fetched in the background. This suits content updates from a CMS webhook.
  • updateTag('products'), used inside a Server Action, refreshes the data straight away so the person who made a change sees it on the next page load.

Combined with <Suspense>, this lets one page mix a cached shell that loads instantly with personalised parts, such as a basket count, that stream in after. The result is static-site speed without stale prices or stock levels.

Upgrading from Next.js 15

If you are on Next.js 15, most of the work in moving to 16 is mechanical, but check these before you start:

  • Node.js 20.9 or later is now required.
  • Turbopack is the default bundler for development and production builds. Builds are much faster, but check any custom webpack configuration still works or has an equivalent.
  • middleware.ts is deprecated in favour of proxy.ts, with the exported function renamed to proxy. Proxy runs on Node.js only, so check anything that relied on the edge runtime.
  • Request APIs are async only. params, searchParams, cookies() and headers() must be awaited. Next.js 15 allowed synchronous access with a warning; 16 removes it.
  • revalidateTag takes a second argument. Pass a cache profile such as 'max'.

The official upgrade codemod (npx @next/codemod@canary upgrade latest) handles much of this. It does not convert synchronous request APIs, so run the separate next-async-request-api codemod too if you still have any. Then run your full test suite and compare Core Web Vitals before and after, because caching behaviour is the area most likely to change without an error.

A quick performance checklist

  • Check field data in Search Console, not only lab scores
  • Keep "use client" at the leaves of the component tree
  • Delay third-party scripts, and load tracking only after consent
  • Give every responsive image an accurate sizes value and enable AVIF
  • Use fetchPriority="high" on the LCP image only
  • Self-host and preload the fonts used above the fold
  • Cache data with "use cache", cacheLife and cacheTag, and refresh it by tag

We build and optimise Next.js sites and web applications for UK businesses. If your site is slow or you are planning an upgrade, see our custom web development services or send us the URL below and we will tell you what we would fix first.

Vishal Shah

Vishal Shah

Technical Adviser, Renav Limited

Vishal has 13+ years of experience in full-stack software development, specialising in Laravel, Next.js and cloud architecture. He leads technical direction at Renav Limited, a UK-based bespoke software agency.

Start a project

Tell us what you're building

Share a few lines about your project and we'll come back with a clear plan and a free quote. No obligation, no hard sell.

Vishal Shah, Technical Adviser at Renav Limited

Your enquiry goes to Vishal Shah

Technical Adviser · replies within 2 business hours

Get your free quote

No obligation. We only use your details to reply to this enquiry.