Next.js Performance Optimization
Speed up Next.js apps by shrinking bundles, lazy loading code, streaming, and measuring Core Web Vitals.
TL;DR
- Keep components on the server and limit
'use client'to interactive parts. - Lazy load heavy client code with
next/dynamicor a dynamicimport(). - Find large dependencies by running
npx next experimental-analyze.
Measure First
Production BuildTest performance on a real build, never on the development server.
npm run build
npm run start
# then run Lighthouse on localhost:3000experimental-analyzeOpen an interactive map of what is inside your bundles and why.
npx next experimental-analyzeuseReportWebVitals()Collect real Core Web Vitals from the browsers of actual visitors.
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric.name, metric.value);
});
return null;
}Ship Less JavaScript
next/dynamicLoad a heavy Client Component only when it is about to render.
'use client';
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), {
loading: () => <p>Loading chart...</p>,
});import()Load a library only when the user triggers the feature that needs it.
async function exportCsv(rows: Row[]) {
const { unparse } = await import('papaparse');
download(unparse(rows));
}Narrow ImportsImport one function instead of a whole library to keep the bundle small.
// Pulls in the whole library
import _ from 'lodash';
// Pulls in one function
import debounce from 'lodash/debounce';optimizePackageImportsLoad only the modules you use from a large package of many exports.
const nextConfig = {
experimental: {
optimizePackageImports: ['my-icon-library'],
},
};Faster Rendering
<Suspense>Stream a slow section later so it never delays the rest of the page.
<Suspense fallback={<Skeleton />}>
<SlowRecommendations />
</Suspense>Promise.all()Fetch independent data in parallel to cut the total wait time.
const [product, reviews] = await Promise.all([
getProduct(id),
getReviews(id),
]);'use cache'Cache expensive work so most requests skip it entirely.
export async function getCatalog() {
'use cache';
cacheLife('hours');
return sql`SELECT * FROM products`;
}reactCompilerTurn on automatic memoization to cut re-renders without manual hooks.
const nextConfig = {
reactCompiler: true,
};Images, Fonts, and Scripts
preloadPreload the largest image at the top of the page to improve LCP.
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
preload
/>next/fontSelf-host fonts and reserve their space, which prevents layout shift.
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });next/scriptDelay third-party scripts until the page is idle so they never block it.
import Script from 'next/script';
<Script
src="https://example.com/widget.js"
strategy="lazyOnload"
/>Tips
- Measure before you optimize by running Lighthouse on a production build, because
next devis much slower than production. - Enable
reactCompilerto memoize components automatically, which removes the need for most manualuseMemoanduseCallbackcalls.
Warnings
- A
'use client'directive high in the tree pulls every imported module into the browser bundle, so place it low. - Importing a whole library such as
lodashfor one function bloats the bundle, so import the single function you need.
In Practice
Keep a large charting library out of the first page load and fetch it only when needed.
- dynamic splits the chart into its own file that loads on demand.
- The loading option shows a placeholder so the layout does not jump.
- The chart only loads after the user asks to see it.
- Visitors who never open the chart never download its code.
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const SalesChart = dynamic(() => import('./SalesChart'), {
loading: () => <div className="h-64 bg-gray-100" />,
});
export function Report({ points }: { points: number[] }) {
const [open, setOpen] = useState(false);
return (
<section>
<button onClick={() => setOpen(true)}>
Show chart
</button>
{open && <SalesChart points={points} />}
</section>
);
}FAQ
Start by measuring with Lighthouse on a production build. Then reduce client JavaScript, lazy load heavy components, optimize images and fonts, and stream slow data with .
Run npx next experimental-analyze on Next.js 16.1 or later to explore the Turbopack bundle. Projects that build with Webpack can use the @next/bundle-analyzer plugin.
They are three measurements of real user experience. LCP measures loading speed, INP measures responsiveness, and CLS measures layout stability.
It can reduce unnecessary re-renders by memoizing components automatically. Enable it with reactCompiler: true. Builds become a little slower because the compiler runs through Babel.