Tech Verse Logo
Enable dark mode
Dynamic Imports and Code Splitting in Next.js 16

Dynamic Imports and Code Splitting in Next.js 16

Md. Mostafijur RahmanMMd. Mostafijur Rahman

Md. Mostafijur Rahman

4 min read

When you import a 300KB charting library or a heavy text editor directly into a Client Component in Next.js 16, every user pays that download cost up front—even if the component sits behind a collapsed tab. Standard static ES imports force bundlers to stitch those packages into the initial page JavaScript. Your First Contentful Paint (FCP) and Interaction to Next Paint (INP) metrics tank, especially on mobile devices running throttled CPUs.

Next.js gives us next/dynamic to split these chunks away from the critical rendering path. Combined with the ssr: false option, you can completely isolate client-only, browser-heavy libraries. But splitting blindly can introduce network waterfalls and layout shifts if you don't measure what you're shipping.

Splitting Client Components with next/dynamic

In Next.js 16 running React 19, next/dynamic acts as a top-level loader that combines React's lazy() and Suspense under a unified API. It creates a separate bundle chunk that standard HTTP requests fetch only when the component mounts in the DOM.

Here's how to isolate a heavy analytics chart component that relies on browser APIs like Canvas and ResizeObserver:

'use client';

import dynamic from 'next/dynamic';
import { useState } from 'react';

const HeavyAnalyticsChart = dynamic(
  () => import('./components/HeavyAnalyticsChart'),
  {
    loading: () => <div className="h-64 w-full bg-slate-100 animate-pulse rounded-lg" />,
    ssr: false,
  }
);

export default function AnalyticsDashboard() {
  const [showMetrics, setShowMetrics] = useState(false);

  return (
    <div className="p-6 text-slate-900">
      <h2 className="text-xl font-bold">Performance Overview</h2>
      <button
        onClick={() => setShowMetrics(!showMetrics)}
        className="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md"
      >
        {showMetrics ? 'Hide Detailed Metrics' : 'Load Detailed Metrics'}
      </button>

      {showMetrics && (
        <div className="mt-6">
          <HeavyAnalyticsChart />
        </div>
      )}
    </div>
  );
}

Passing ssr: false tells Next.js to skip server-side pre-rendering for this module entirely. The server renders only the skeleton provided in the loading fallback function. Once the HTML hits the browser and React hydrates the parent tree, the client issues a fetch request for the component's split JavaScript chunk.

The Server Component Trap with ssr: false

A common gotcha in Next.js 16 involves placing ssr: false directly inside a React Server Component (RSC). Server Components do not run in the browser client runtime; they render exclusively on the server or during build time. If you call dynamic(() => import(...), { ssr: false }) inside a file without the 'use client' directive, Next.js throws an explicit build error:

Error: ssr: false is not supported with Server Components. Move this dynamic import into a Client Component.

To fix this, keep your Server Components clean and pass server-fetched data as props into a dedicated Client Component wrapper where next/dynamic handles the deferred execution.

Measuring the Payload with @next/bundle-analyzer

Refactoring code without measuring bundle output is guessing. You need to verify that Next.js actually extracted the heavy dependency into its own standalone chunk instead of leaking it back into main-app-[hash].js or the page's shared chunk.

Install the official bundle analyzer package:

npm install @next/bundle-analyzer

Then update your next.config.mjs configuration to wrap your export with the analyzer plugin when an environment variable is set:

// next.config.mjs
import bundleAnalyzer from '@next/bundle-analyzer';

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
});

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
};

export default withBundleAnalyzer(nextConfig);

Run your production build with analysis turned on:

ANALYZE=true npm run build

Next.js generates two interactive HTML reports in your .next/analyze directory: one for the browser bundles and one for the Node.js server bundles. Open client.html in your browser.

Reading the Numbers

Look for three values on every parsed node in the visual map:

  • Stat size: The raw, uncompressed source code before minification.
  • Parsed size: The minified code output by SWC. This is what the browser parses and compiles.
  • Gzipped size: The wire size sent across the network.

On a recent dashboard refactor using Next.js 16 and React 19, direct static imports of Monaco Editor and Recharts resulted in an initial JavaScript payload of 480KB (gzipped). By wrapping both modules in next/dynamic with ssr: false and rendering them conditionally upon tab selection, the initial client bundle dropped to 135KB. On a throttled 4G network test on a mid-range mobile device, Total Blocking Time dropped from 380ms down to 40ms, and INP improved from 290ms to 35ms.

Trade-offs: Layout Shifts and Request Waterfalls

Code splitting is not free. When you defer component loading, you trade initial bundle size for runtime network roundtrips and potential visual shifts.

1. Cumulative Layout Shift (CLS)

If your loading fallback has a height of 0px, loading a 400px chart component will push layout elements down the viewport when the JS chunk arrives and executes. Always give your loading fallback matching layout dimensions using explicit Tailwind height classes or CSS aspect ratio properties.

2. Network Waterfalls

If Component A dynamically imports Component B, and Component B dynamically imports Component C, you create a sequential chain of network fetches: render A -> download B -> render B -> download C. The user sits waiting through three roundtrips. Keep dynamic imports shallow. Top-level dynamic imports at feature boundaries (tabs, modals, drawer panels, below-the-fold widgets) provide maximum bundle reduction with minimal latency penalty.

3. Event-Driven Dynamic Imports

Sometimes you don't even need next/dynamic. For action-based payloads like exporting PDF files or downloading CSV datasets, standard dynamic ES imports inside event handlers work better:

'use client';

export default function ExportButton({ data }) {
  const handleExport = async () => {
    // Load xlsx library only when user clicks export
    const XLSX = await import('xlsx');
    const worksheet = XLSX.utils.json_to_sheet(data);
    const workbook = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(workbook, worksheet, 'Data');
    XLSX.writeFile(workbook, 'export.xlsx');
  };

  return (
    <button
      onClick={handleExport}
      className="px-4 py-2 bg-emerald-600 text-white rounded-md hover:bg-emerald-700"
    >
      Export to Excel
    </button>
  );
}

In this pattern, zero code for the spreadsheet generation library is included in the initial render or pre-rendered HTML. The browser fetches the 150KB library chunk only when the user clicks the button, keeping your page lightweight and responsive.

Md. Mostafijur RahmanMMd. Mostafijur Rahman

WRITTEN BY

Md. Mostafijur Rahman

    Latest Posts

    View All

    Fixing Next.js Hydration Mismatch Errors in React 19

    Fixing Next.js Hydration Mismatch Errors in React 19

    Generating Open Graph Images at the Edge in Next.js 16

    Generating Open Graph Images at the Edge in Next.js 16

    Fix Next.js Third Party Script Performance

    Fix Next.js Third Party Script Performance

    Next.js 16 Testing Strategy: Unit, Component, E2E

    Next.js 16 Testing Strategy: Unit, Component, E2E

    Shrinking Next.js Bundle Size: Analyzer & Barrel Fixes

    Shrinking Next.js Bundle Size: Analyzer & Barrel Fixes

    Next.js 16 App Router Auth Patterns with Laravel 12

    Next.js 16 App Router Auth Patterns with Laravel 12

    Next.js i18n Routing: Locales, Hreflang, and Metadata

    Next.js i18n Routing: Locales, Hreflang, and Metadata

    Next.js 16 Caching Architecture: The Four Layers

    Next.js 16 Caching Architecture: The Four Layers

    Dynamic Imports and Code Splitting in Next.js 16

    Dynamic Imports and Code Splitting in Next.js 16

    Next.js Image Optimization: Sizes and Remote Patterns

    Next.js Image Optimization: Sizes and Remote Patterns