Technology · Next.js

Next.js Performance Optimization

Profile apps, optimize bundle size, and implement advanced caching and streaming.

TL;DR
  1. 01Use dynamic imports to split code and reduce bundles.
  2. 02Enable compression and minification in production.
  3. 03Implement streaming for faster first contentful paint.

Bundle Size Analysis

  • Check bundle size with @next/bundle-analyzer.
    npm install -D @next/bundle-analyzer
  • Configure next.config.js to analyze bundles.
    const withBundleAnalyzer = require("@next/bundle-analyzer")({
      enabled: process.env.ANALYZE === "true",
    });
    
    module.exports = withBundleAnalyzer({});
  • Run analysis to find large dependencies.
    ANALYZE=true npm run build
  • Look for duplicate or unused dependencies to remove.
  • Use ANALYZE=true npm run build to open an interactive bundle treemap in the browser.

Code Splitting Strategies

  • Use dynamic imports to split code at route level.
    import dynamic from "next/dynamic";
    
    const HeavyComponent = dynamic(() => import("./HeavyComponent"));
    
    export default function Page() {
      return <HeavyComponent />;
    }
  • Split large components loaded conditionally.
    const Editor = dynamic(() => import("./Editor"), {
      loading: () => <p>Loading editor...</p>,
    });
  • Lazy load images with the Image component.
    import Image from "next/image";
    
    <Image
      src="/hero.jpg"
      alt="Hero"
      loading="lazy"
      width={1200}
      height={600}
    />
  • Mark third-party libraries as external to exclude them from the bundle.
    // next.config.js
    module.exports = {
      experimental: { optimizePackageImports: ["lodash", "date-fns"] }
    };
  • Use barrel file tree-shaking to avoid importing entire libraries.
    // Bad: imports entire lodash
    import _ from "lodash";
    // Good: imports only what you need
    import debounce from "lodash/debounce";

Caching Optimization

  • Use immutable cache headers for static assets.
    // next.config.js
    module.exports = {
      onDemandEntries: {
        maxInactiveAge: 60 * 1000,
        pagesBufferLength: 5,
      },
    };
  • Configure stale-while-revalidate for optimal freshness.
    export const revalidate = 3600; // 1 hour
    
    // ISR with background updates
    const res = await fetch("https://api.example.com/data", {
      next: { revalidate: 60 }
    });
  • Set cache-control headers for API routes.
    export async function GET(request: Request) {
      return Response.json(data, {
        headers: {
          "Cache-Control": "public, s-maxage=3600"
        }
      });
    }
  • Use unstable_cache to cache server-side function results across requests.
    import { unstable_cache } from "next/cache";
    
    const getCachedUser = unstable_cache(
      async (id: string) => db.user.findUnique({ where: { id } }),
      ["user"], { revalidate: 300 }
    );
  • Tag cached data so you can revalidate it on demand with revalidateTag.
    const data = await unstable_cache(fetchPosts, ["posts"], {
      tags: ["posts"]
    })();
    
    // elsewhere: revalidateTag("posts") clears this cache

Compression and Minification

  • Enable Gzip compression for responses.
    // next.config.js
    module.exports = {
      compress: true, // enabled by default
    };
  • Use React strict mode to catch performance issues.
    // next.config.js
    module.exports = {
      reactStrictMode: true,
    };
  • Minify HTML, CSS, and JavaScript automatically.
    npm run build
  • Check production bundle size after optimization.
  • Remove console statements in production with compiler options.
    // next.config.js
    module.exports = {
      compiler: { removeConsole: { exclude: ["error"] } }
    };

Streaming and Advanced Techniques

  • Use streaming to send content progressively.
    import { Suspense } from "react";
    
    export default function Page() {
      return (
        <div>
          <Header />
          <Suspense fallback={<p>Loading...</p>}>
            <SlowComponent />
          </Suspense>
        </div>
      );
    }
  • Prefetch important routes and links.
    import Link from "next/link";
    
    <Link href="/dashboard" prefetch={true}>
      Dashboard
    </Link>
  • Use the Image component for automatic optimization.
    import Image from "next/image";
    
    <Image
      src="/photo.png"
      alt="Photo"
      width={500}
      height={300}
      quality={80}
    />
  • Monitor Core Web Vitals with web-vitals library.
    import { getCLS, getFID, getFCP, getLCP, getTTFB } from "web-vitals";
    
    getCLS(console.log);
    getFID(console.log);
  • Export reportWebVitals from app to send metrics to an analytics endpoint.
    // app/layout.tsx
    export function reportWebVitals(metric: NextWebVitalsMetric) {
      fetch("/api/vitals", { method: "POST", body: JSON.stringify(metric) });
    }

Tip: Use Next.js's built-in performance features like Image optimization, code splitting, and ISR before implementing custom solutions.

Warning: Don't over-optimize prematurely — measure performance with real-world tools and focus on the metrics that matter most to users.

Next.js Metadata and SEONext.js Streaming and Progressive Rendering