Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

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.

Utilities
Quick take
Typical fix time 25 min
  • 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
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. Code splitting means users only download and parse the code they actually need for the current page, dramatically improving Time to Interactive.

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 KB gzipped 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.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Rules that often go hand-in-hand with this one.

Minify all JavaScript files

All JavaScript files are minified to reduce file size and improve loading performance.

JavaScript
Use ES modules (import/export)

Use native ES module syntax for imports and exports instead of CommonJS require() to enable static analysis, tree-shaking, and better tooling support.

JavaScript
Load scripts with defer, async, or type=module

Prevent JavaScript from blocking HTML parsing by using defer, async, or type=module attributes on script tags so the browser can continue building the DOM while scripts download.

HTML
Prevent common memory leak patterns

Identify and avoid the most common JavaScript memory leak sources: forgotten event listeners, retained DOM references, closures holding large objects, and uncleared timers.

JavaScript

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385