Tree Shaking
why your "tiny" import still ships 70 KB
Web developmentweb development / build tooling
The symptom
You import one helper:
import { debounce } from 'lodash'
Rebuild for production… and the bundle grows by ~70 KB. You wanted ~2 KB. Where did the rest come from? You "tree-shook" it, right?
Reasoning it out
Tree shaking = the bundler statically DELETES exported code you never use. Key word: statically. It only removes a thing when it can PROVE the thing is unused. Two things routinely destroy that proof:
- CommonJS.
require()is dynamic — exports are resolved at runtime, so the bundler can't know which ones you touch and keeps them ALL. Classiclodashships as CommonJS. Soimport { debounce }still drags in the whole module graph. - Side effects. If a module runs code just by being imported (a polyfill,
import './styles.css', registering a global), deleting it could change behavior — so the bundler must keep it, even if you use nothing from it.
The fix
- Use an ESM / per-method build so the export graph is static:
import debounce from 'lodash-es' // or: 'lodash/debounce'
→ drops to ~2 KB.
- Tell the bundler which files are safe to drop, in package.json:
"sideEffects": false
or list the exceptions:
"sideEffects": ["*.css"]
- If you author a library, ship ESM ("module" field / "exports" map), not CJS.
Nuances practitioners get wrong
import * as _ from 'lodash-es'can DEFEAT shaking — namespace imports are harder to prove unused. Prefer named/default imports.- Barrel files (
index.tsre-exporting 50 modules) + one side-effecting module = the whole barrel stays. - It's a PRODUCTION optimization. Dev builds keep everything, so "it's huge in dev" is not the bug — measure the prod build.
- Marking
"sideEffects": falseon a file that DOES have side effects = silent breakage (a missing polyfill, unregistered global). Only claim it when it's true.
Prove it to yourself
npx source-map-explorer dist/*.js
Look for a module you "only used a little of" but that shows up huge.
Your exercise
Take a project importing from CJS lodash. Switch one import to lodash-es (or lodash/fn), rebuild for production, and diff the bundle size. Then add "sideEffects": false to a small internal package and confirm nothing breaks. What was the KB delta — and did anything quietly break?