Understanding Next.js Caching Layers (App Router)

The App Router introduced a multi-layered caching system designed to make applications as fast as possible by default.

  1. Data Cache: This is a persistent cache for the results of your data fetching operations (e.g., fetch requests). When you fetch data in a Server Component, the result is cached. Subsequent requests for the same data will hit the cache instead of the original data source. You control this with revalidate options.
  2. Full Route Cache: At build time or after the first visit, Next.js can render an entire page—including the result of data fetches and the React Server Component payload—and cache it on the server. This makes navigating between statically rendered pages instant.
  3. Client-side Router Cache: This is an in-memory cache in the user's browser. When a user navigates between pages, Next.js caches the rendered result. If the user clicks back to a previously visited page, it loads instantly from memory without a new server request.

Analyze Your Bundle Size

One of the biggest culprits of slow websites is a large JavaScript bundle. You need to know what's inside it. @next/bundle-analyzer is a tool that helps you visualize the size of your JavaScript bundles.

Setup:

  1. Install the package: npm install @next/bundle-analyzer
  2. Configure next.config.js:
  3. JavaScript

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({})
  1. Run the build with the ANALYZE flag: ANALYZE=true npm run build

This will open an interactive treemap in your browser, showing you exactly which libraries are contributing the most to your bundle size. You can then make informed decisions about whether to replace a heavy library or code-split it.

Code Splitting with next/dynamic

Sometimes you have a large component or library that isn't needed on the initial page load (e.g., a heavy charting library, a modal, or a rich text editor). Instead of bundling it with the main page, you can dynamically import it.

next/dynamic is a special wrapper that makes this easy. It tells Next.js to create a separate JavaScript chunk for this component and only load it when it's actually rendered.

Code Example:

JavaScript


"use client";
import { useState } from 'react';
import dynamic from 'next/dynamic';

// This will create a separate 'heavy-chart-component.js' bundle
const HeavyChartComponent = dynamic(() => import('../components/HeavyChart'), {
  loading: () => <p>Loading chart...</p>, // Display a loading state
  ssr: false, // This component will only be rendered on the client-side
});

export default function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>Show Expensive Chart</button>
      {/* The JS for HeavyChartComponent is only downloaded when showChart becomes true */}
      {showChart && <HeavyChartComponent />}
    </div>
  );
}

Font and Script Optimization

  • next/font: Automatically optimizes local or Google Fonts, self-hosting them to eliminate requests to external servers and preventing layout shifts.
  • next/script: Provides a component to load third-party scripts with different strategies (beforeInteractive, afterInteractive, lazyOnload) to avoid blocking page rendering.