Load heavy code on use

Keep heavy, gated and third-party code out of the main chunk.

From vercel-react-best-practices: bundle-dynamic-imports, bundle-conditional and 1 more.

Monaco in the main chunk

Load both, then click Open editor before each hydrates.

Static importBad
main.js435 KB

Not loaded

Dead clicks: 0
import { MonacoEditor } from './monaco-editor'
next/dynamicGood
main.js135 KB
monaco-editor.jsnot requested

Not loaded

Dead clicks: 0
dynamic(() => import('./monaco-editor'), { ssr: false })
import dynamic from 'next/dynamic'

const MonacoEditor = dynamic(
  () => import('./monaco-editor').then(m => m.MonacoEditor),
  { ssr: false, loading: () => <EditorSkeleton /> }
)

function CodePanel({ code }: { code: string }) {
  const [open, setOpen] = useState(false)
  return (
    <>
      <button onClick={() => setOpen(true)}>Open editor</button>
      {open && <MonacoEditor value={code} />}
    </>
  )
}

A feature most leave off

Toggle each switch on, off, then on again.

In the initial payloadBad
Initial JS
315 KB
animation-frames.js
in main.js

Animation off

import { frames } from './animation-frames.js'
import() on enableGood
Initial JS
135 KB
animation-frames.js
not requested

Animation off

import('./animation-frames.js')
function AnimationPlayer() {
  const [enabled, setEnabled] = useState(false)
  const [frames, setFrames] = useState<Frame[] | null>(null)

  useEffect(() => {
    if (enabled && !frames && typeof window !== 'undefined') {
      import('./animation-frames.js')
        .then(mod => setFrames(mod.frames))
        .catch(() => setEnabled(false))
    }
  }, [enabled, frames])

  return (
    <>
      <Switch checked={enabled} onCheckedChange={setEnabled} />
      {enabled && (frames ? <Canvas frames={frames} /> : <Skeleton />)}
    </>
  )
}

Analytics after hydration

Drag the third-party size, then Replay.

90 KB
Static importBad
HTML
main.js
hydrate
analytics

Interactive at 0.00 s

import { Analytics } from '@vercel/analytics/react'
dynamic, ssr: falseGood
HTML
main.js
hydrate
analytics

Interactive at 0.00 s

dynamic(() => import('@vercel/analytics/react')…, { ssr: false })
import dynamic from 'next/dynamic'

const Analytics = dynamic(
  () => import('@vercel/analytics/react').then(m => m.Analytics),
  { ssr: false }
)

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  )
}

Why it matters, pros and cons

Why it matters

Buttons ignore clicks until the main chunk loads. Keep one-panel code out of it.

Pros

  • Earlier Time to Interactive
  • Unused features are never downloaded
  • Split chunks cache independently

Cons

  • First open waits a round trip; preload on hover
  • Never lazy-load the LCP element
  • Tiny splits add requests; split at tens of KB