All topics
Performanceintermediate

Code Splitting and Lazy Loading Modules

Breaking a JavaScript bundle into smaller chunks loaded on demand, using dynamic import() to reduce the initial page load's JavaScript payload.

Code splitting breaks a bundle into chunks loaded on demand. import() is the mechanism.

Code splitting is like a restaurant that doesn't print and hand every guest its entire encyclopedic menu including seasonal items, off-menu specials, and the dessert list all at once — it hands you the core menu immediately so you can start ordering right away, and only brings out the dessert menu (a separate chunk) later, specifically once you actually ask for it.

Key Concepts

1
    // loaded at startup
    import Chart from './chart.js';
2
    // loaded when this line runs
    const { default: Chart } = await import('./chart.js');
3
The bundler sees the dynamic import and emits a separate chunk.
4
Why it matters. Every kilobyte in the initial bundle must be downloaded, parsed and executed before the page is interactive. Parsing is the part people forget — it is CPU work, and on a mid-range phone it dominates.
5
A dashboard shipping a charting library to users who never open the chart pays that cost on every visit.
6
The three places to split.By route. The largest win, and the easiest:
7
    const Settings = lazy(() => import('./Settings'));
8
By interaction. A modal, an editor, a map — loaded when opened.
9
    button.onclick = async () => {
      const { openEditor } = await import('./editor.js');
      openEditor();
    };
10
Heavy libraries. A date library, a PDF generator, a syntax highlighter — imported inside the function that needs them.
11
Preloading removes the delay. Splitting trades bundle size for a wait at the moment of use. Fetching the chunk on hover, or during idle time, gets both:
12
    link.onmouseenter = () => import('./Settings');
13
Do not split too finely. Each chunk is a request with its own overhead, and a page pulling in twenty small chunks can be slower than one reasonable bundle over a high-latency connection.
14
Handle the failure. A chunk request can fail — a flaky network, or a deploy that removed the old file:
15
    import('./chart.js').catch(() => showFallback());
16
That second case is real: a user with the page open during a deploy requests a chunk that no longer exists, and the feature silently does nothing without a handler.
17
Measure before splitting. webpack-bundle-analyzer, or Vite's rollup-plugin-visualizer, shows what is actually large. The answer is usually one or two dependencies rather than the application code, and the fix is often replacing a library rather than splitting it.
webpack-bundle-analyzerrollup-plugin-visualizer
18
What the interviewer is probing.1. "What does dynamic import give you?" Probing: the mechanism. Stalls: "Async loading." Moves up: a promise for the module and a separate bundle chunk, so the code is not in the initial download.
19
2. "Where should you split?" Probing: judgement. Stalls: "Everywhere." Moves up: at route boundaries and around heavy rarely-used features; splitting too finely adds request overhead.
20
3. "What does parse cost mean on mobile?" Probing: the real constraint. Stalls: "Download size." Moves up: JavaScript must be parsed and executed as well as downloaded, and on a mid-range phone that is often the dominant cost.
21
4. "How do you avoid the loading delay?" Probing: prefetching. Stalls: "You cannot." *Moves up:* prefetch the chunk during idle time so it is cached before the user navigates.