Next.js Performance: Where the Time Actually Goes

June 2, 2026

Next.js Performance: Where the Time Actually Goes

Performance advice tends to arrive as a numbered list of tips, which is a shame, because the tips are rarely the problem. The problem is usually that nobody has measured where the time goes.

Before optimising anything, get a bundle report and a trace. Most apps have one or two dominant costs, and they are almost never the thing that was about to be optimised.

Ship less JavaScript

The single largest lever in a Next.js app is how much client-side JavaScript reaches the browser. Every "use client" boundary marks a subtree that must be downloaded, parsed, and hydrated.

The instinct is to mark the page as a client component because something deep inside needs state. That pulls the entire tree across the boundary. Push the boundary down instead — keep the page a server component and isolate the interactive part:

// Server component: no JavaScript shipped for any of this. import { AddToCart } from "./add-to-cart"; export default async function ProductPage() { const product = await getProduct(); return ( <article> <h1>{product.name}</h1> <p>{product.description}</p> {/* Only this subtree is a client component. */} <AddToCart productId={product.id} /> </article> ); }

The description, the heading, and the layout cost nothing on the client. Only the button ships.

Watch what your imports drag in

A single import can undo a lot of careful boundary work. Date libraries, icon sets, and validation schemas are common offenders, particularly when imported as a namespace:

// Pulls the whole library into the bundle. import * as icons from "some-icon-pack"; // Pulls one icon. import { ChevronRight } from "some-icon-pack";

Run the analyzer before guessing:

ANALYZE=true pnpm build

Two things worth checking in the output: modules appearing in more than one chunk, and any dependency you cannot immediately justify. Both are usually cheap to fix and the win is disproportionate.

Get the caching semantics right

Caching is where correctness and speed collide, and where the defaults deserve a deliberate decision rather than a shrug.

StrategyFitsTrade-off
force-cacheContent that changes on deployStale until redeployed
revalidate: nContent with a tolerable staleness windowSome users see old data
Tag-based revalidationContent invalidated by a known eventRequires wiring the event
no-storePer-user or real-time dataEvery request hits the origin

Tag-based revalidation is underused. When you know the event that makes data stale — an order placed, a post published — invalidating on that event beats guessing at a time window.

Images and fonts are usually free wins

next/image handles sizing, format negotiation, and lazy loading. The part that matters most is marking your largest above-the-fold image with priority, so it is not lazily deferred — that image is frequently the Largest Contentful Paint element, and deferring it directly delays the metric.

Fonts are similar: load them through next/font so they are self-hosted and preloaded, rather than fetched from a third-party origin mid-render. Both changes are small and neither has a downside worth weighing.

Stream instead of blocking

A page that awaits every data source before rendering anything is as slow as its slowest query. Wrapping the slow region in Suspense lets the rest of the page render immediately:

import { Suspense } from "react"; export default function Dashboard() { return ( <> <Header /> <Suspense fallback={<StatsSkeleton />}> {/* Slow query no longer blocks the header. */} <Stats /> </Suspense> </> ); }

The total work is unchanged. What changes is when the user starts seeing something, which is what they actually experience.

Things that rarely help

  • Micro-optimising renders in a tree that is not the bottleneck. Memoisation has a cost of its own.
  • Adding a cache layer in front of a query that was never slow.
  • Chasing a perfect Lighthouse score on a synthetic run while field data says something different.

Wrap-up

The levers that move Next.js performance are, in rough order: how much JavaScript you ship, what your caching actually does, whether the LCP image is prioritised, and whether slow data blocks fast data.

Measure first. The bundle report is unglamorous and it will tell you more than any list of tips, including this one.

GitHub
LinkedIn
Instagram