Skip to content
Architecture Lab · LAB-09transitive dependencies / tree shaking

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 laboratory

Add 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

  1. 01The package ships CommonJS. require is dynamic, so the bundler cannot statically prove which exports are unused.
  2. 02The package has no "sideEffects": false field, so the bundler must assume importing any file has observable effects.
  3. 03A barrel file re-exports everything, and one re-exported module has a side effect at module scope.
  4. 04The import is a namespace import that is then indexed dynamically.
  5. 05The code is only reachable at runtime through a dynamic key, so nothing can be proven dead.
package.jsonjson
{ "type": "module", "sideEffects": false, "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }, "./utils": "./dist/utils.js" }}
What a well-behaved package declares
NumberMeansWho cares
Install sizeEverything on disk in node_modulesCI time, disk, install speed
Package sizeThe published tarballRegistry and install speed
Bundle impactBytes added to the shipped outputYour users, the only one they feel
Parse/execute costCPU time on the deviceINP 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

  1. 01MDNTree shaking
  2. 02webpacksideEffects
  3. 03Node.jsPackage entry points, exports