Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Code Splitting

Code splitting breaks a large JavaScript bundle into smaller chunks that are loaded on demand. This reduces initial bundle size, improves Time to Interactive (TTI), and ensures users only download the code they actually need.

Why Code Splitting Matters

Shipping a single large bundle forces users to download and parse all JavaScript upfront, including code for routes and features they may never visit. Code splitting addresses this by creating lazy-loaded chunks loaded at runtime.

MetricWithout SplittingWith Splitting
Initial JS size500 KB (all routes)120 KB (entry + current route)
TTISlow (parse all JS)Fast (parse only needed code)
Cache hit rateLow (any change invalidates all)High (unchanged chunks stay cached)

Dynamic Import

The import() syntax is the foundation of code splitting — it returns a Promise and tells bundlers to create a separate chunk:

// Dynamic import — creates a separate chunk
button.addEventListener('click', async () => {
  const { Chart } = await import('./chart-module.js');
  const chart = new Chart(container);
});
// React lazy loading with Suspense
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Route path="/dashboard" component={Dashboard} />
      <Route path="/settings" component={Settings} />
    </Suspense>
  );
}

Splitting Strategies

Route-Based Splitting

Split at route boundaries — each page becomes a chunk. This is the most common and impactful strategy since users only load the page they visit.

Component-Based Splitting

Split heavy components within a page (e.g., a rich text editor, a 3D viewer) that are conditionally rendered or below the fold.

Library Splitting

Separate large third-party libraries (lodash, moment, chart.js) into vendor chunks with longer cache TTLs. Webpack’s SplitChunksPlugin handles this:

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

Bundle Analysis

Before optimizing, measure. Use these tools to identify large chunks and opportunities:

ToolOutputBest For
webpack-bundle-analyzerInteractive treemapIdentifying large dependencies
source-map-explorerSource-level size breakdownFinding which code contributes most
bundlesizeCI check for size thresholdsPreventing bundle regression
LighthousePerformance score with suggestionsOverall load performance
// webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin;

module.exports = {
  plugins: [new BundleAnalyzerPlugin()]
};

Prefetching and Preloading

Complement code splitting with resource hints:

// Prefetch: low priority, loads for likely next navigation
import(/* webpackPrefetch: true */ './SettingsPage');

// Preload: high priority, loads for current page
import(/* webpackPreload: true */ './HeroChart');

Interview Questions

Q1: What is code splitting and why is it important? A: Code splitting divides a JavaScript bundle into smaller chunks loaded on demand. It reduces the initial payload, improves TTI and FCP, and increases cache efficiency by keeping stable vendor chunks cached while only app code changes.

Q2: What is the difference between route-based and component-based splitting? A: Route-based splitting creates one chunk per route/page — users load only the current page’s code. Component-based splitting extracts heavy components (editors, charts) within a page that are conditionally rendered. Route-based is the default starting point; component-based is for further optimization.

Q3: How does dynamic import() differ from static import? A: Static import is resolved at build time and bundles the module eagerly. Dynamic import() is resolved at runtime, returns a Promise, and tells the bundler to create a separate chunk. This enables lazy loading — code is only fetched when the import is executed.

Q4: What is the vendor chunk pattern? A: Third-party libraries change less frequently than application code. Extracting them into a separate vendors chunk means that when you deploy app changes, users don’t re-download the unchanged libraries. Webpack’s SplitChunksPlugin with a node_modules regex test automates this.

Cross-References

References