Next.js provides useful performance primitives, but their defaults do not remove the need for measurement. A page can use the App Router and still be slow because of a late LCP image, a broad client boundary, uncached data, third-party scripts, or expensive rendering.
This guide targets Next.js 16. Caching and image APIs changed across recent versions, so check the matching documentation when maintaining an older application.
Measure the production route
Test the exact route and state users receive. Record whether it is static, cached, streamed, or rendered per request; whether the visitor is signed in; and which cookies, experiments, and third parties are active.
Use a repeated Lighthouse lab test for a controlled baseline, then inspect Chrome DevTools Performance and Network panels for the cause. Field Core Web Vitals answer a different question and should be monitored separately. How to Check Website Speed describes the complete workflow.
Instrument server work as well. A slow route can spend time in proxy logic, authentication, database queries, upstream fetch() calls, cache misses, or React rendering before the browser receives useful HTML.
Optimize images for their actual layout
The next/image component can generate responsive image URLs, reserve intrinsic space, and lazy-load images by default. It still needs accurate dimensions and layout information.
import Image from "next/image";
import dashboard from "./dashboard.png";
export function ProductHero() {
return (
<Image
src={dashboard}
alt="Product dashboard showing weekly revenue and active users"
sizes="(max-width: 768px) 100vw, 1100px"
preload
/>
);
}
In Next.js 16, priority is deprecated in favor of preload. Reserve preload for the image that is genuinely likely to become LCP; the Image API documentation cautions against combining it with loading or fetch-priority controls. Most below-the-fold images should keep the default lazy behavior.
For fill, give the parent a defined layout and supply a realistic sizes value. Without sizes, the browser may select a larger candidate than the rendered image needs. Restrict remote image sources, and configure cache behavior to match how often source images can change.
If LCP is still late, inspect the LCP phases. An optimized image cannot overcome slow server response, client-only discovery, a redirect chain, or render delay from CSS and JavaScript.
Keep the client boundary small
App Router pages and layouts use Server Components by default. Add "use client" only at a boundary that needs state, effects, event handlers, or browser APIs. Everything imported beneath that boundary becomes part of the client module graph.
// app/products/[slug]/page.tsx
import { BuyButton } from "./buy-button";
export default async function ProductPage({ params }: PageProps<"/products/[slug]">) {
const { slug } = await params;
const product = await getProduct(slug);
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<BuyButton productId={product.id} />
</main>
);
}
Keep data loading and static presentation on the server when appropriate, and pass serializable props to focused interactive components. Do not move an entire page to the client just because one button needs an event handler.
Server Components can reduce client JavaScript, but they do not automatically make server work fast. Parallelize independent data requests, remove request waterfalls, select only needed data, and stream sections only when the loading experience is coherent.
Choose an explicit caching model
Next.js 16 has two documented caching models. Do not mix snippets without knowing which one the application uses.
With Cache Components enabled
Cache Components are opt-in through cacheComponents: true. Cached functions or components use the use cache directive with APIs such as cacheLife and cacheTag:
import { cacheLife, cacheTag } from "next/cache";
export async function getCatalog() {
"use cache";
cacheLife("hours");
cacheTag("catalog");
return db.product.findMany({ where: { published: true } });
}
After a catalog mutation, use revalidateTag("catalog", "max") when stale-while-revalidate behavior is acceptable, or updateTag("catalog") inside a Server Action for immediate read-your-own-writes behavior. The current revalidation guide documents those semantics.
Without Cache Components
In the previous model, fetch() is not cached by default. Opt individual requests into caching and revalidation only when their data can be shared safely. The previous-model caching guide documents the supported options.
In either model, define freshness from product requirements. Never cache authorization-dependent or account-specific output as if it were public. For self-hosted multiple instances, verify that cache storage and invalidation behave consistently across processes and deployments.
Load fonts and scripts intentionally
next/font downloads supported font assets at build time and serves them from the application domain. Use only the families, subsets, weights, and styles required by the design. Variable fonts can reduce the number of font files when they cover the needed range.
import { Geist } from "next/font/google";
const geist = Geist({
subsets: ["latin"],
display: "swap",
});
The font optimization guide describes self-hosting and layout integration. Font optimization reduces avoidable network and layout costs, but verify the actual fallback and rendered weights; do not assume every custom font combination produces zero movement.
Use next/script according to when the feature is needed. afterInteractive is suitable for scripts that should load after hydration; lazyOnload can delay lower-priority code until browser idle time. beforeInteractive is for narrowly defined critical scripts and should not become the default for analytics.
Measure each third party with consent and production settings enabled. A deferred script can still create long tasks after it downloads.
Inspect the production bundle
Build the application in production mode and inspect what reaches the client. Next.js supports @next/bundle-analyzer, but a bundle visualization is only the start.
Look for:
- a client boundary that imports a large server-capable subtree;
- duplicate packages or multiple versions;
- whole-library imports when a focused entry point exists;
- editors, charts, maps, or media tools loaded before use;
- locale data and polyfills unnecessary for the target browsers;
- third-party scripts outside the module bundle.
Use dynamic imports for genuinely optional client features, and show an accessible loading state when delayed. Do not split tiny modules indiscriminately; extra chunks and request coordination also have cost.
Treat Turbopack as a build tool
Turbopack is the stable default for both next dev and next build in Next.js 16. The Next.js 16 upgrade guide documents the change; the --turbopack flag is no longer required.
Faster builds and refreshes improve development and delivery workflows, but enabling Turbopack does not by itself make a page's runtime metrics better. Runtime performance still depends on the emitted client code, server behavior, assets, caching, and third parties. If the project relies on unsupported webpack plugins, review compatibility before switching rather than assuming equivalent output.
Use a release checklist
Before shipping a Next.js performance change, verify:
- the Server and Client Component boundary matches real interactivity;
- the LCP image is discoverable, correctly sized, and not lazy-loaded;
- only one intentional LCP candidate uses preload behavior;
- the application uses one documented caching model with safe invalidation;
- fonts contain only needed variants;
- third-party scripts have an explicit loading strategy;
- production bundles and request waterfalls were inspected;
- repeated lab runs improved under equivalent conditions;
- field Core Web Vitals remain the production measure of real experience.
Review dated measurements on IndieTools Speed. For framework trade-offs, continue with Next.js vs Nuxt: Which Is Faster?; for score interpretation, see What Is a Good PageSpeed Score?.


