awesome-everything RU
↑ Back to the climb

Performance

Tree shaking and compression: removing what you don''''t use

Crux Tree shaking strips unused exports at build time — but four common pitfalls silently defeat it. Brotli and gzip reduce wire bytes without touching parse cost.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 14 min

A developer imports one function from lodash: import { debounce } from 'lodash'. The bundle analyzer shows 70 KB of lodash in the output. They expected 2 KB. The import looked right — but the module format was wrong.

How tree shaking works

Tree shaking is the bundler’s ability to detect and remove exports that are never imported. If a module exports 200 functions and the app imports only one, the other 199 are “dead code” — they should not appear in the bundle.

This works because ES module syntax (import / export) is statically analysable: the bundler can trace import chains at build time without executing code. CommonJS (require()) computes exports at runtime; the bundler cannot know statically which ones are used, so it includes everything.

// Tree-shakeable (ESM) — bundler sees at build time which exports are used
import { debounce } from 'lodash-es';  // Only debounce in the bundle

// NOT tree-shakeable (CJS) — bundler must include all exports
import { debounce } from 'lodash';      // All 70 KB of lodash in the bundle

To enable tree shaking: use the library’s ESM build (often listed as module in package.json), configure the bundler to use it (resolve.mainFields: ['module', 'main'] in Webpack, automatic in Vite), and ensure your own code uses named exports.

Four pitfalls that defeat tree shaking

Pitfall 1 — side effects at module scope. A module that sets window.foo = bar or registers a global when imported cannot be tree-shaken — the bundler must preserve the side effect even if no export is consumed. Fix: annotate the package as "sideEffects": false in package.json if it has no module-scope side effects, or use /* #__PURE__ */ annotations on specific calls.

Pitfall 2 — CommonJS re-export. Even if your code is ESM, a library that compiles to CJS cannot be tree-shaken. The bundler wraps the whole module. Check the library’s package.json: if there is no "module" or "exports" field pointing to an ESM build, the whole library ships. Switch to an ESM-first alternative or use cherry-picked imports (lodash/debounce instead of lodash).

Pitfall 3 — export * re-exports. export * from './module' re-exports everything. If even one consumer imports one export from this barrel, the entire module is kept. Fix: use explicit named re-exports in barrel files.

Pitfall 4 — dynamic import with a variable. await import('./module') is tree-shakeable; await import(name) where name is a runtime variable is not — the bundler cannot know which module will be requested at build time.

PitfallWhy it defeats tree shakingFix
Side effects at module scopeBundler must preserve global mutationsideEffects: false or #PURE annotation
CJS libraryExports computed at runtimeUse ESM build or cherry-pick imports
export * barrel filesOne consumer keeps entire moduleExplicit named re-exports
Dynamic import with variableModule unknown at build timeUse string literals in import()

Compression: gzip vs brotli

Compression reduces the bytes transferred over the wire. Text-based assets (JS, CSS, JSON, SVG) compress 60-80% with gzip and 70-90% with brotli. The same 500 KB uncompressed JS file becomes roughly 200 KB gzip’d or 160 KB brotli’d.

Important constraint: compression reduces download cost only. Parse, compile, and execute cost is determined by the uncompressed size of the code. Switching from gzip to brotli saves ~10-25% more on the wire — worth doing, but it does not address the CPU bottleneck.

Static vs dynamic compression: Static compression generates the compressed file at build time and serves it directly with Content-Encoding: br. Cost at request time: zero. Dynamic compression compresses on the fly per request: ~5-30 ms for brotli, ~1-5 ms for gzip. Senior pattern: static brotli for all static assets (JS, CSS); dynamic gzip for HTML responses that vary per user.

# Verify brotli is served
curl -sI -H "Accept-Encoding: br" https://your-app.com/app.js | grep content-encoding
# Expected: content-encoding: br
Why this works

Why does the uncompressed size determine parse cost? The browser decompresses the file before parsing. The JS engine receives and parses the original bytes, not the compressed form. Gzip and brotli are transport-layer optimisations — they save network bytes but the CPU work is the same.

Quiz

A library ships only a CJS build. The team imports one utility function. What happens in the bundle?

Quiz

A team switches from gzip to static brotli for their JS assets. Transfer size drops 15%. LCP improves 40 ms on mobile. Is the parse cost affected?

Quiz

A module has `window.analytics = createAnalytics()` at module scope and is marked sideEffects: true. The app only imports one function from it. What happens?

Recall before you leave
  1. 01
    Why does importing from a CJS library ship the whole library even if you only use one function?
  2. 02
    A library is marked sideEffects: false but still appears fully in the bundle. What are two likely causes?
  3. 03
    What is the difference in effect between gzip and code splitting on parse cost?
Recap

Tree shaking relies on ESM static analysis to remove unused exports. The four pitfalls — CJS modules, module-scope side effects, barrel export*, and dynamic variable imports — silently defeat it and are worth auditing in every build. Compression (brotli preferred over gzip) reduces wire bytes but leaves parse cost unchanged. Together, tree shaking and compression complement code splitting: splitting reduces what a route downloads; tree shaking removes dead code within each chunk; compression shrinks the transfer. The next lesson covers third-party scripts — the category that most commonly blows through carefully set budgets.

Connected lessons
appears again in159
Continue the climb ↑Third-party scripts: the silent budget killer
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.