You installed 4KB. It brought 87 friends.
A dependency graph, weighed honestly: transitive depth, duplicate versions and the tree-shaking that did not happen.
- Level
- SYSTEMS
- Read time
- 09 min
- Experiment
- Available
- Type
- INVESTIGATION
The question
Why does adding one small utility increase the bundle by far more than its own size, and why does the same library appear three times?
Hypothesis
Install size, transitive depth and shipped bytes are three different numbers. Only the third one matters to users, and it depends on module format and how the package is authored.
Method
Laboratory available
The instrument for this investigation runs in the laboratory, where the controls, the live model and the observation log share one workstation.
Enter the laboratoryAdd packages to the graph and watch the transitive closure, the duplicate versions and the estimated shipped weight.
What we observed
Two packages depending on different major versions of a third do not deduplicate, both copies ship. This is the usual explanation for a bundle that grew by 90KB when you added something described as tiny.
Why it happens
Why tree shaking fails
- 01The package ships CommonJS.
requireis dynamic, so the bundler cannot statically prove which exports are unused. - 02The package has no
"sideEffects": falsefield, so the bundler must assume importing any file has observable effects. - 03A barrel file re-exports everything, and one re-exported module has a side effect at module scope.
- 04The import is a namespace import that is then indexed dynamically.
- 05The code is only reachable at runtime through a dynamic key, so nothing can be proven dead.
{ "type": "module", "sideEffects": false, "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }, "./utils": "./dist/utils.js" }}| Number | Means | Who cares |
|---|---|---|
| Install size | Everything on disk in node_modules | CI time, disk, install speed |
| Package size | The published tarball | Registry and install speed |
| Bundle impact | Bytes added to the shipped output | Your users, the only one they feel |
| Parse/execute cost | CPU time on the device | INP and time to interactive |
Further research
The strongest lever is rarely a lighter alternative, it is deleting the need. Date formatting, unique ids, deep clone, debounce and query-string parsing all have adequate platform equivalents now: Intl.DateTimeFormat, crypto.randomUUID(), structuredClone(), and URLSearchParams. The best dependency is the one you did not need.
References
- 01MDNTree shaking
- 02webpacksideEffects
- 03Node.jsPackage entry points, exports