HAMZA KAREEM / CONCEPTS hamzawi.dev

Lesson 331 min read

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:

  1. CommonJS. require() is dynamic — exports are resolved at runtime, so the bundler can't know which ones you touch and keeps them ALL. Classic lodash ships as CommonJS. So import { debounce } still drags in the whole module graph.
  2. 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.ts re-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": false on 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?