# Dynamic Imports in Next.js: A 30-Second Performance Win
By Govind Bajaj · 2025

Tags: next.js, performance, dynamic-imports, code-splitting, web-performance
The checkout page loaded 340KB of charting library on every visit. Dynamic imports cut the initial bundle to 12KB. The charts loaded when needed. Here's the one-liner.
The Sans Herbals admin dashboard loads a sales chart on the overview page.

The chart uses Recharts, which pulls in D3 utilities and SVG rendering code. The total: 340KB of JavaScript. This loaded on every admin visit, even when the user never navigated to the analytics tab.

Dynamic imports reduced the initial bundle to 12KB. The 340KB chart library loads only when the user clicks the Analytics tab. The implementation is one line of code.

## The Problem with Static Imports

```tsx
// Before: charts.js is in the main bundle
import SalesChart from '@/components/SalesChart'

export default function AdminDashboard() {
  return (
    <div>
      <DashboardOverview />
      <SalesChart /> {/* 340KB, loaded even if below the fold */}
    </div>
  )
}
```

Static imports are included in the initial JavaScript bundle. The browser downloads, parses, and executes them before the page becomes interactive. Heavy components delay hydration and hurt INP.

## The One-Line Fix

```tsx
// After: charts.js loads only when needed
import dynamic from 'next/dynamic'

const SalesChart = dynamic(() => import('@/components/SalesChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false
})

export default function AdminDashboard() {
  const [showAnalytics, setShowAnalytics] = useState(false)
  
  return (
    <div>
      <DashboardOverview />
      <button onClick={() => setShowAnalytics(true)}>
        View Analytics
      </button>
      {showAnalytics && <SalesChart />}
    </div>
  )
}
```

The import() function returns a Promise. next/dynamic wraps it in a React component that suspends while loading. The loading prop shows a fallback. The component code is split into a separate JavaScript chunk that loads on demand.

## Dynamic Imports for Conditional Components

Different user roles need different UI. Do not bundle all of them:

```tsx
import dynamic from 'next/dynamic'

const AdminPanel = dynamic(() => import('./AdminPanel'))
const EditorPanel = dynamic(() => import('./EditorPanel'))

function Dashboard({ role }) {
  if (role === 'admin') return <AdminPanel />
  return <EditorPanel />
}
```

Each panel is a separate chunk. The admin panel's heavy management tools do not burden editors.

## Dynamic Imports for Heavy Libraries

Rich text editors, charting libraries, PDF viewers, and code editors are all candidates:

```tsx
const TipTapEditor = dynamic(() => import('@/components/TipTapEditor'), {
  ssr: false,
  loading: () => <div className="h-64 bg-gray-100 animate-pulse" />
})

const PDFViewer = dynamic(() => import('@/components/PDFViewer'), {
  ssr: false
})
```

## The Loading State Matters

A skeleton that matches the final component's dimensions prevents layout shift:

```tsx
function ChartSkeleton() {
  return (
    <div className="w-full h-[400px] bg-gray-100 rounded-lg animate-pulse" />
  )
}

const SalesChart = dynamic(() => import('./SalesChart'), {
  loading: ChartSkeleton
})
```

Without a loading state, the component renders nothing while loading, then suddenly appears — a layout shift that hurts CLS.

## Measuring the Impact

Use the Next.js bundle analyzer to see before and after:

```bash
npm install -D @next/bundle-analyzer
ANALYZE=true npm run build
```

The analyzer shows every chunk and its size. Look for large libraries that are only used on specific routes — those are your dynamic import candidates.

## Takeaways

- next/dynamic splits heavy components into separate JavaScript chunks
- Always provide a loading component that matches final dimensions to prevent CLS
- Use ssr: false for components that depend on window or browser APIs
- Conditionally rendered components (tabs, modals, role-based UI) are prime candidates
- Run the bundle analyzer to find the biggest dynamic import opportunities
- This is a 30-second code change with potentially massive bundle impact