All topics
Performanceadvanced

Bundle Size Optimization and Tree Shaking

Techniques bundlers use to eliminate unused code from a final JavaScript bundle, relying heavily on ES modules' static import/export structure.

Tree shaking removes exports nothing imports. It depends on ES modules being statically analysable.

Tree shaking is like a moving company that, instead of packing an entire warehouse's full inventory onto the truck because you rented space there, only loads the specific labeled boxes you've actually indicated you need at your new place, based on a complete, itemized list you provided upfront (the static import graph) — nothing gets shipped that wasn't explicitly itemized as needed.

Key Concepts

1
    // utils.js
    export function used() {}
    export function unused() {}
2
    // main.js
    import { used } from './utils.js';
    // unused is not in the bundle
3
Why CommonJS cannot be shaken.
    const { used } = require('./utils');    // resolved at runtime
4
require can be conditional, computed, or wrapped. The bundler cannot prove what is needed, so it includes everything.
require
5
That is the practical argument for ES modules, beyond syntax.
6
Side effects block it. A module that does something at import time cannot be removed even if nothing uses its exports:
7
    // analytics.js
    setupTracking();          // a side effect
8
    "sideEffects": false      // in package.json -- I have none
    "sideEffects": ["*.css"]  // except these
9
Marking this correctly is what lets a library be shaken properly, and getting it wrong is why some libraries ship entirely regardless of what you import.
10
Import shape matters.
    import _ from 'lodash';           // the whole library
    import { debounce } from 'lodash';       // often still the whole
    import debounce from 'lodash/debounce';  // just this
    import { debounce } from 'lodash-es';    // shakeable
11
lodash is CommonJS; lodash-es is modules. The same library, with very different results.
lodashlodash-es
12
Measure before optimising.
    webpack-bundle-analyzer
    rollup-plugin-visualizer
    source-map-explorer
13
The output almost always shows one or two dependencies dominating, and the fix is replacing or splitting those rather than trimming application code.
14
The usual large offenders. A full date library where a few functions would do, an icon set imported wholesale, a charting library used for one chart, and a polyfill bundle for browsers you no longer support.
15
What else reduces the download.
    minification        standard
    compression         Brotli over gzip
    modern syntax       do not transpile for browsers you do not support
    code splitting      covered separately
16
And the number worth tracking. Bundle size in CI, with a budget that fails the build. Without it, size grows a little with every feature and nobody notices until the page is slow.
17
What the interviewer is probing.1. "What does tree shaking require?" Probing: the precondition. Stalls: "A modern bundler." Moves up: ES modules with static imports, so unused exports can be proven unreachable — CommonJS resolves at runtime and cannot be shaken.
18
2. "What is a side effect in this context?" Probing: the blocker. Stalls: "A bug." *Moves up:* module-level code that runs on import, such as a polyfill; without sideEffects: false the bundler must keep it.
19
3. "How do you find what is making a bundle big?" Probing: the method. Stalls: "Guess." Moves up: a bundle analyser on the build stats — the answer is almost always one or two dependencies.
20
4. "What import style defeats tree shaking?" Probing: the habit. Stalls: "None." Moves up: importing a whole namespace or a CommonJS build, which pulls everything in regardless of use.