Next.js Performance Optimization

Speed up Next.js apps by shrinking bundles, lazy loading code, streaming, and measuring Core Web Vitals.

TL;DR

  1. Keep components on the server and limit 'use client' to interactive parts.
  2. Lazy load heavy client code with next/dynamic or a dynamic import().
  3. Find large dependencies by running npx next experimental-analyze.

Measure First

    Production Build

    Test performance on a real build, never on the development server.

    npm run build
    npm run start
    # then run Lighthouse on localhost:3000
    experimental-analyze

    Open an interactive map of what is inside your bundles and why.

    npx next experimental-analyze
    useReportWebVitals()

    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/dynamic

    Load 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 Imports

    Import 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';
    optimizePackageImports

    Load 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`;
    }
    reactCompiler

    Turn on automatic memoization to cut re-renders without manual hooks.

    const nextConfig = {
      reactCompiler: true,
    };

Images, Fonts, and Scripts

    preload

    Preload the largest image at the top of the page to improve LCP.

    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      preload
    />
    next/font

    Self-host fonts and reserve their space, which prevents layout shift.

    import { Inter } from 'next/font/google';
    
    const inter = Inter({ subsets: ['latin'] });
    next/script

    Delay 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

  1. Measure before you optimize by running Lighthouse on a production build, because next dev is much slower than production.
  2. Enable reactCompiler to memoize components automatically, which removes the need for most manual useMemo and useCallback calls.

Warnings

  1. A 'use client' directive high in the tree pulls every imported module into the browser bundle, so place it low.
  2. Importing a whole library such as lodash for one function bloats the bundle, so import the single function you need.

In Practice

FAQ