Load heavy code on demand

Keep editors, charts and 3D out of the main bundle; import() on use.

From nextjs-pagespeed.

The same page, two bundles

Press Reload, then click Open editor in both panes.

Static importBad
Notes
Editor JS at load
—
Blocking time at load
—
Button responds from
—
Click → editor
—
import { Editor } from "./editor"
Dynamic importGood
Notes
Editor JS at load
—
Blocking time at load
—
Button responds from
—
Click → editor
—
dynamic(() => import("./editor"))
"use client";
import dynamic from "next/dynamic";
import { useState } from "react";
import { EditorSkeleton } from "./editor-skeleton";

// Own chunk, fetched the first time <Editor> renders
const Editor = dynamic(() => import("./editor").then((m) => m.Editor), {
  ssr: false,
  loading: () => <EditorSkeleton />,
});

export function Notes() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button type="button" onClick={() => setOpen((o) => !o)}>Open editor</button>
      {open && <Editor />}
    </>
  );
}

Start the import on intent

Hover or press each Open chart button, then release.

import() on clickFine
Revenue
    Head start
    —
    Wait after click
    —
    import() on intentBest
    Revenue
      Head start
      —
      Wait after click
      —
      // Same specifier as dynamic(): the click reuses the in-flight request
      const preload = () => void import("./chart");
      
      <button
        type="button"
        onPointerEnter={preload}
        onFocus={preload}
        onPointerDown={preload} // touch has no hover
        onClick={() => setOpen(true)}
      >
        Open chart
      </button>

      Reserve the space it will take

      Press Load page and watch the paragraph below.

      One-line “Loading…”Bad

      Q3 revenue

      September closed at 90, the best month this year. Recurring revenue made up most of the gain.

      Churn stayed flat.

      CLS—
      Skeleton at final sizeGood

      Q3 revenue

      September closed at 90, the best month this year. Recurring revenue made up most of the gain.

      Churn stayed flat.

      CLS—
      const Chart = dynamic(() => import("./chart"), {
        loading: () => <div className="chart-slot" aria-busy="true" />,
      });
      
      .chart-slot { block-size: 170px; }

      Why it matters, pros and cons

      Why it matters

      Every first-load byte is parsed by every visitor, and the first tap waits.

      Pros

      • Less first-load JS, fewer long tasks.
      • Only feature users pay, once.
      • Prefetch on intent hides most waiting.

      Cons

      • First open waits a round trip without prefetch.
      • An unsized placeholder trades blocking time for shift.
      • Small modules cost more as a separate request.