Module Bundling
Date: 2026-08-17
Following imports to build a graph, then emitting it as a small number of files. The interesting parts are what gets left out — tree-shaking — and how the graph is cut into chunks, because both decide what a visitor actually downloads.
Bundling resolves a module graph from an entry point and emits it as one or more output files.
Building the graph
main.js
├─ imports basket.js
│ ├─ imports price.js
│ └─ imports lodash
└─ imports header.js
└─ imports price.js ← already
in the graph
Each module appears once, however many times it’s imported. The graph is the bundler’s model of the application, and everything after is a transformation of it.
Module formats
| Format | Syntax | Static analysis |
|---|---|---|
| ESM | import / export | Yes — the whole point |
| CommonJS | require() / module.exports | No — dynamic |
| UMD | Wrapper detecting the environment | No |
| IIFE | A self-executing function | N/A |
ESM’s static structure is what makes tree-shaking possible. Imports are declarations resolvable without running the code, so a bundler can prove an export is unused. The language-level differences between the two systems are in Modules.
// analysable — a bundler knows what's used
import { debounce } from 'lodash-es'
// not — the argument could be anything
const name = getName()
const mod = require(name)This is why the ecosystem moved to ESM, and why a CommonJS-only dependency costs you tree-shaking on that package.
Tree-shaking, and why it under-delivers
Tree-shaking removes exports nothing imports. It frequently removes less than expected, for specific reasons:
- Side effects. A module that mutates global state on import can’t be removed, even if its exports are unused.
sideEffects: falseinpackage.jsontells the bundler it’s safe - CommonJS dependencies can’t be analysed statically
- Barrel files. An
index.jsre-exporting a hundred modules means importing one pulls the barrel, and whether the rest is shaken depends on the side-effect declarations of all of them - Whole-library imports:
import _ from 'lodash' // the lot
import { debounce } from 'lodash-es' // just thisBarrel files are the most common cause of a bundle being larger than expected, and they’re a convention almost every codebase adopts for import tidiness.
Chunks
The graph has to be cut into files, and where you cut decides the caching behaviour:
ONE BUNDLE
everything.js ← one request
← any change invalidates all of it
SPLIT BY ROUTE
main.js
checkout.js ← loaded when needed
account.js
SPLIT BY CHANGE FREQUENCY
vendor.js ← changes rarely
app.js ← changes daily
← vendor stays cached across deploys
Splitting vendor code from application code is the highest-value split, because dependencies change far less often than your own code, and the cached copy survives every deploy — Code Splitting, Caching Strategies.
Content hashing
app.a3f9c2e1.js
The hash is derived from the file’s contents, so changing the content changes the URL. That makes the file cacheable indefinitely, because a new version is a new URL rather than a stale one — Content Delivery Networks.
The trap: if a shared chunk’s hash changes on every build — because a module ID or the ordering shifted — every file’s cache is invalidated on every deploy. Deterministic module IDs matter for exactly this reason — Build Caching.
Output formats
esm modern browsers and bundlers
cjs Node, older tooling
iife a <script> tag, no module system
umd works everywhere, largest
For an application, emit ESM and let the tooling handle older targets. For a library, ship both ESM and CJS, with exports in package.json mapping them — a library shipping only CJS denies its consumers tree-shaking.
Where it goes wrong
- Duplicate dependencies. Two versions of the same package resolved into one bundle.
npm ls <pkg>finds it - A dependency bundled that should be external. A library bundling React ships a second copy into every consumer
- Node built-ins in browser code. Something imports
pathorcryptoand the bundler either polyfills it silently or fails - No measurement. The only reliable way to know what’s in a bundle is to look — Bundle Analysis