Split large JavaScript bundles
Use dynamic imports and route-based code splitting to break large bundles into smaller chunks that load on demand, reducing initial page load time.
- Use dynamic import() to load code only when it's actually needed
- Split routes in SPAs so each page loads only its own code
- Lazy-load heavy third-party libraries (chart libraries, rich text editors)
- Analyze your bundle with a visualizer to find what to split
Rule Details
Code splitting uses import() (opens in new tab) and route boundaries to divide your JavaScript application into separate chunks that load on demand rather than all at once.
Code Example
// ❌ Static import loads everything upfront
import { generatePDF } from './pdf-generator.js' // Heavy library loaded even if never used
// ✅ Dynamic import loads only when needed
async function handleExportClick() {
const { generatePDF } = await import('./pdf-generator.js')
generatePDF(document)
}Why It Matters
A 500 KB JavaScript bundle blocks page interactivity for 3–5 seconds on a mid-range mobile device even after the bytes arrive — JS must be parsed and compiled before execution. The web.dev dynamic imports guide (opens in new tab) focuses on this exact win: users only download and parse the code they actually need for the current page.
Route-Based Splitting (React)
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
// Each route is its own chunk — users download code for the page they visit
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
const Reports = lazy(() => import('./pages/Reports'))
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
)
}Lazy-Loading Heavy Components
// Rich text editors, chart libraries, map components
const RichEditor = lazy(() => import('./RichEditor'))
const ChartPanel = lazy(() => import('./ChartPanel'))
function PostEditor({ showChart }) {
return (
<div>
<Suspense fallback={<EditorSkeleton />}>
<RichEditor />
</Suspense>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<ChartPanel />
</Suspense>
)}
</div>
)
}Conditional Feature Loading
// Load analytics only in production
if (process.env.NODE_ENV === 'production') {
import('./analytics.js').then(({ init }) => init())
}
// Load a polyfill only when needed
async function setupApp() {
if (!window.ResizeObserver) {
await import('resize-observer-polyfill')
}
initApp()
}Preloading for Anticipated Navigation
// Preload the next likely page without executing it yet
function prefetchSettingsPage() {
import(/* webpackPrefetch: true */ './pages/Settings')
}
// Trigger on hover — user is likely about to click
settingsLink.addEventListener('mouseenter', prefetchSettingsPage)Analyzing Your Bundle
A first pass with Webpack Bundle Analyzer (opens in new tab) or Bundlephobia (opens in new tab) usually shows which dependency or route should move behind import().
# With Vite
pnpm exec vite-bundle-visualizer
# With webpack
pnpm exec webpack-bundle-analyzer stats.json
# With source-map-explorer
pnpm exec source-map-explorer 'build/static/js/*.js'Verification
Automated Checks
- Compare the before/after output from your bundle visualizer and confirm the initial chunk shrinks meaningfully;
import()(opens in new tab) should move real code out of the initial graph rather than only reshuffle file names. - Test the loading state or suspense fallback so deferred features still feel intentional to users.
- Re-run Lighthouse or your performance budget check to confirm the split improves initial JS cost instead of only moving bytes around.
- If your project uses a JS budget, keep the initial bundle within that threshold rather than only moving bytes into slightly later chunks; a common starting point is
<= 150 KBgzipped for the main route bundle.
Manual Checks
- Verify the lazy-loaded code is not downloaded on first load unless the current route or interaction needs it.
Use with AI
Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.
Check
Verify implementation
Identify any large third-party imports or feature modules in this file that could be loaded lazily with dynamic import() instead of statically.
Fix
Auto-fix issues
Convert the identified static imports to dynamic imports using import() and add appropriate loading states.
Explain
Learn more
Explain JavaScript code splitting, how dynamic import() works, and how to implement route-based splitting in a SPA.
Review
Code review
Inspect route modules, heavy feature imports, and third-party libraries loaded on initial render. Flag dependencies that can move behind `import()`, route boundaries, or user-triggered flows without breaking the first meaningful paint.