NextjsLazy Loading & next/dynamic

Lazy Loading & next/dynamic

Not every component a page renders needs to be part of the initial JavaScript bundle. A modal that only opens when a user clicks a button, a heavy chart library, a rich text editor used on one admin page — all of these can be split out and loaded only when they are actually needed. Next.js provides next/dynamic as a thin wrapper around React.lazy and Suspense to make that easy.
A worked example

components/HeavyChart.tsx

TSX
export default function HeavyChart({ data }: { data: number[] }) {
  // imagine this pulls in a large charting library
  return <div>{/* render chart from data */}</div>
}

app/dashboard/page.tsx

TSX
import dynamic from 'next/dynamic'

const HeavyChart = dynamic(() => import('../../components/HeavyChart'), {
  loading: () => <p>Loading chart...</p>,
})

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <HeavyChart data={[1, 2, 3]} />
    </div>
  )
}
Instead of bundling HeavyChart into the page's main JavaScript, Next.js emits it as a separate chunk that's only fetched when DashboardPage actually renders it. The loading option lets you show a placeholder while that chunk downloads.
Common use cases

Situation

Why lazy load

A modal, drawer, or dialog

Its code is dead weight until the user triggers the action that opens it

A library that only works in the browser

Load it with ssr: false to skip server rendering entirely for components that touch window or browser-only APIs

A rarely visited admin or settings page

Keeps its dependencies out of the bundle for every visitor who never goes there

Disabling server rendering for a browser-only component

TSX
import dynamic from 'next/dynamic'

const MapWithNoSSR = dynamic(() => import('../../components/Map'), {
  ssr: false,
  loading: () => <p>Loading map...</p>,
})
Note
ssr: false is only valid in a Client Component context — a component that relies on browser-only globals like window or document would throw during server rendering otherwise, so skipping SSR for it avoids that crash entirely.
Dynamically importing a modal on demand

A modal that only loads its code when opened

TSX
'use client'

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

const ConfirmModal = dynamic(() => import('./ConfirmModal'))

export default function DeleteButton() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Delete
      </button>
      {open && <ConfirmModal onClose={() => setOpen(false)} />}
    </>
  )
}
ConfirmModal's code never reaches the browser until open becomes true — for a page where most visitors never click "Delete," that's bundle weight most users never pay for.
Tip
Don't guess at what to lazy load. Run @next/bundle-analyzer against your production build to see which chunks are actually large, and target next/dynamic at the components responsible — splitting a component that's already tiny just adds an extra network request for no benefit.
  • next/dynamic splits a component into its own chunk, fetched only when it actually renders.

  • Good candidates: modals, heavy libraries, and rarely visited routes' dependencies.

  • ssr: false skips server rendering for components that depend on browser-only APIs.

  • The loading option renders a placeholder while the chunk downloads.

  • Use bundle analysis, not intuition, to find which components are worth lazy loading.