Large client libraries loaded synchronously block the initial page load. Use next/dynamic or React.lazy to code-split and load them on demand.
Impact: HIGH (reduces initial JavaScript bundle by deferring heavy libraries until needed)
Importing heavy client-side libraries at the top of a file forces the entire library into the initial bundle — even if the component is below the fold, behind a tab, or conditionally rendered. next/dynamic (Next.js's wrapper around React.lazy) code-splits these imports so they load only when needed.
Common offenders: chart libraries (Chart.js, Recharts), rich text editors (TipTap, Slate), map libraries (Leaflet, Mapbox), PDF viewers, syntax highlighters, and markdown renderers.
Incorrect (everything in the initial bundle):
'use client'
// ❌ 200KB+ loaded immediately even if chart is below the fold
import { Chart } from 'chart.js/auto'
import { Bar } from 'react-chartjs-2'
// ❌ 500KB+ rich text editor loaded on a page where most users just read
import { Editor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
// ❌ Heavy map library loaded on every page load
import { MapContainer, TileLayer, Marker } from 'react-leaflet'
export default function AnalyticsPage() {
return (
<div>
<h1>Dashboard</h1>
<Bar data={chartData} />
<Editor extensions={[StarterKit]} />
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="..." />
</MapContainer>
</div>
)
}Correct (dynamic imports with loading states):
'use client'
import dynamic from 'next/dynamic'
// ✅ Code-split heavy components — loaded only when rendered
const BarChart = dynamic(
() => import('react-chartjs-2').then(mod => ({ default: mod.Bar })),
{
loading: () => <div className="h-64 animate-pulse bg-gray-100 rounded" />,
ssr: false, // Chart.js requires browser APIs
}
)
const RichEditor = dynamic(
() => import('@/components/RichEditor'),
{
loading: () => <div className="h-48 animate-pulse bg-gray-100 rounded" />,
ssr: false, // Editor requires DOM APIs
}
)
const Map = dynamic(
() => import('@/components/LeafletMap'),
{
loading: () => <div className="h-96 animate-pulse bg-gray-100 rounded" />,
ssr: false, // Leaflet requires window
}
)
export default function AnalyticsPage() {
return (
<div>
<h1>Dashboard</h1>
<BarChart data={chartData} />
<RichEditor />
<Map center={[51.505, -0.09]} zoom={13} />
</div>
)
}// ✅ Conditionally load heavy components
'use client'
import dynamic from 'next/dynamic'
import { useState } from 'react'
const PdfViewer = dynamic(() => import('@/components/PdfViewer'), {
ssr: false,
})
export default function DocumentPage({ document }) {
const [showPdf, setShowPdf] = useState(false)
return (
<div>
<h1>{document.title}</h1>
<p>{document.summary}</p>
{/* PDF viewer only loads when user clicks the button */}
<button onClick={() => setShowPdf(true)}>View PDF</button>
{showPdf && <PdfViewer url={document.pdfUrl} />}
</div>
)
}When to use ssr: false:
window, document, or browser-only APIsWhen NOT to dynamically import:
Detection hints:
# Find heavy library imports in client components
grep -rn "'use client'" src/ --include="*.tsx" -l | \
xargs grep -l "chart\|editor\|leaflet\|mapbox\|pdf\|monaco\|codemirror\|markdown"Reference: Next.js Lazy Loading · React.lazy
reduces initial JavaScript bundle by deferring heavy libraries until needed
BeforeMerge scans your pull requests against this rule and 6+ others. Get actionable feedback before code ships.