Dynamic Imports
Load code only when needed. Modals, charts, and heavy components don't belong in your initial bundle.
Core Principle
Code that isn't needed immediately shouldn't be in your initial bundle. Dynamic imports load code only when it's actually needed.
The Basic Pattern
import dynamic from 'next/dynamic';
// Instead of: import HeavyChart from '@/components/HeavyChart';
const HeavyChart = dynamic(() => import('@/components/HeavyChart'));Now HeavyChart loads in a separate chunk, downloaded only when rendered.
With Loading State
const HeavyChart = dynamic(
() => import('@/components/HeavyChart'),
{
loading: () => <div className="h-64 animate-pulse bg-gray-200" />,
}
);Client-Only Components
Some components use window or document and can't render on the server:
const MapComponent = dynamic(
() => import('@/components/Map'),
{ ssr: false }
);ssr: false means no SEO for that content. Fine for maps and widgets. Bad for main content.
When to Use Dynamic Imports
| Component Type | Dynamic? | Why |
|---|---|---|
| Modals/dialogs | Yes | Not visible initially |
| Charts/graphs | Yes | Heavy, usually below fold |
| Rich text editors | Yes | Very heavy, few users need it |
| Navigation | No | Needed immediately |
| Hero section | No | Above fold, needs SEO |
| Auth forms | Maybe | If behind a click |
Pattern: Lazy Modal
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const SettingsModal = dynamic(() => import('./SettingsModal'));
export function SettingsButton() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Settings</button>
{open && <SettingsModal onClose={() => setOpen(false)} />}
</>
);
}Modal code only downloads when button is clicked.
Pattern: Load on Scroll
'use client';
import { useEffect, useState, useRef } from 'react';
import dynamic from 'next/dynamic';
const HeavySection = dynamic(() => import('./HeavySection'));
export function LazySection() {
const [visible, setVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ rootMargin: '100px' }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<div ref={ref}>
{visible ? <HeavySection /> : <Skeleton />}
</div>
);
}Component loads 100px before it scrolls into view.
Pattern: Feature-Gated Loading
import dynamic from 'next/dynamic';
const AdminPanel = dynamic(() => import('./AdminPanel'));
export default function Dashboard({ user }) {
return (
<div>
<MainContent />
{user.role === 'admin' && <AdminPanel />}
</div>
);
}Regular users never download admin code.
Pattern: Conditional Heavy Library
'use client';
import { useState } from 'react';
export function ExportButton({ data }) {
const [exporting, setExporting] = useState(false);
const handleExport = async () => {
setExporting(true);
// Import xlsx only when needed (500KB+ library)
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');
setExporting(false);
};
return (
<button onClick={handleExport} disabled={exporting}>
{exporting ? 'Exporting...' : 'Export to Excel'}
</button>
);
}The xlsx library only loads when someone actually exports.
Quick Checklist
Modals use dynamic imports Modal code shouldn't load until user clicks open.
Charts/graphs are lazy loaded Heavy visualization libraries load on demand.
Heavy libraries imported on-demand xlsx, pdf-lib, etc. load only when needed.
Below-fold sections load on scroll IntersectionObserver triggers loading before visible.
Admin/premium features gated Don't ship code most users will never use.
Next: SSR Patterns - understand server vs client components.