Skip to content
UT Studio

How to optimise a Next.js website

Where the time actually goes, and the handful of changes that move the numbers.

UT Studio8 min read

Most Next.js performance advice optimises things that were never the bottleneck. Before changing anything, find out where the time goes — on the deployed origin, not on localhost, where everything is instant and nothing is compressed the way it will be.

Measure first, and measure the right thing

  • Run Lighthouse against production, not the dev server. The dev server is a different application.
  • Use the network panel with throttling on. A 4G profile is closer to your median user than your laptop is.
  • Look at the build output. Next prints per-route JavaScript; the routes at the top of that list are where to work.

Ship less JavaScript

The largest single lever, and it is architectural rather than a setting. Every component that does not need state, an effect or a browser API should be a Server Component — and then it costs zero bytes.

Client islands
// Client boundary at the leaf, not the branch.
export function ProductPage({ product }) {
  return (
    <article>
      <Overview product={product} />       {/* server */}
      <Specs product={product} />          {/* server */}
      <AddToCart offers={product.offers} /> {/* 'use client' */}
    </article>
  );
}

A client boundary is inherited

Everything imported by a `use client` component becomes client code too. One `use client` at the top of a page ships the whole page — this is the most common cause of a bundle nobody can explain.

Fonts

Self-host through `next/font`. It removes a DNS lookup and a connection to a third party on the critical path, and it generates the size-adjust descriptors that stop the layout jumping when the real font arrives.

app/fonts.ts
import { Inter } from 'next/font/google';

export const sans = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-sans',
});

Images

  • Use `next/image` so dimensions are reserved and modern formats are served.
  • Set `priority` on the one image in the initial viewport, and on no others.
  • Give every image explicit dimensions. Reserved space is the whole of layout stability.
  • Do not lazy-load the hero image. Lazy-loading the largest contentful paint delays the metric it is measured by.

Render statically wherever the content allows

If the pageThenCost per request
Is the same for everyoneStaticNone
Has a known set of routesSSG with paramsNone after build
Reads search params or a sessionDynamicReal — make it fast

What usually does not help

  1. Adding a state management library. It is not the bottleneck and it is more client JavaScript.
  2. Micro-optimising server render. It is normally a few milliseconds against hundreds spent on the network.
  3. Prefetching everything. It competes for bandwidth with what the user is actually waiting for.
  4. Replacing a working CSS approach with a faster one. Stylesheets are rarely the problem.

Re-measure after every change

Performance work without measurement is decoration. Half of the changes people are confident about turn out to be neutral.

Templates mentioned here

Everything above is written against real products. These are the ones this page draws on.

Read next