12.6 · difficulty 3/4 · 12 min read
Tree-shaking in Practice
Tree-shaking is not magic — it has prerequisites. How the sideEffects field affects optimization, what code patterns prevent tree-shaking, and how to verify that tree-shaking is actually working.
How tree-shaking works
Tree-shaking is a bundler capability: it removes exports that are never used from your code.
// utils.ts
export function add(a: number, b: number) {
return a + b
}
export function multiply(a: number, b: number) {
return a * b
}
// main.ts
import { add } from "./utils"
console.log(add(1, 2))
// multiply is never imported → tree-shaking removes itPrerequisite: only ES Modules (import / export) can be tree-shaken. CommonJS (require / module.exports) cannot.
The sideEffects field
{
"name": "@my/utils",
"sideEffects": false
}sideEffects: false tells the bundler:
Every module in this package is side-effect-free — if you haven't imported any of its exports, it is completely safe to remove the import of this file.
What counts as a side effect:
- Modifying global objects (
window.myLib = ...) - Registering global event listeners
- Polyfills
- CSS imports
If CSS files have side effects:
{
"sideEffects": [
"*.css",
"./src/polyfills.ts" // Only these two types have side effects
]
}Common patterns that prevent tree-shaking
Problem 1: CommonJS format
// ❌ CommonJS: cannot be tree-shaken
const { debounce } = require("lodash")
// ✅ ESM: can be tree-shaken
import { debounce } from "lodash-es"Problem 2: Dynamic property access
// ❌ Dynamic access: bundler can't know which method is used
const key = "debounce"
const fn = lodash[key]
// ✅ Static: analyzable
import { debounce } from "lodash-es"Problem 3: Side-effectful initialization code
// ❌ Top-level code with side effects
console.log("Module loaded!") // This line won't be removed even if nothing is imported
export function foo() {
/* ... */
}Problem 4: Class methods
// ❌ Class methods cannot be individually tree-shaken
class MyUtils {
add(a: number, b: number) {
return a + b
}
multiply(a: number, b: number) {
return a * b
}
}
// ✅ Standalone functions can be tree-shaken
export function add(a: number, b: number) {
return a + b
}
export function multiply(a: number, b: number) {
return a * b
}Verifying that tree-shaking works
Method 1: Inspect the output files
pnpm build
grep -r "multiply" dist/ # If there's output, it wasn't tree-shakenMethod 2: Use a bundle analyzer
Use rollup-plugin-visualizer to check whether the multiply function appears in the output.
Method 3: Use a dedicated tool
pnpm add -D rollup-plugin-treeshaker # community toolSelf-check
- Why can't CommonJS format be tree-shaken?
- Under what circumstances does
sideEffects: falsecause styles to disappear? - In the following code, which parts will be tree-shaken away?
export const VERSION = "1.0.0" // is imported
export function usedFn() {
/* ... */
} // is imported
export function unusedFn() {
/* ... */
} // is not imported
const init = () => {
window.LIB = VERSION
}
init() // top-level call, has side effects- Why are standalone functions better for tree-shaking than class methods?
// The following is a utility library. Analyze the tree-shaking behavior:
// The consumer only imports formatDate
// Library utils.ts:
export function formatDate(date: Date): string {
return date.toISOString().slice(0, 10)
}
export function parseDate(str: string): Date {
return new Date(str)
}
// Has a side effect: modifies the Date prototype (this is bad!)
Date.prototype.toReadable = function () {
return this.toLocaleString()
}
// Question 1: Will parseDate be tree-shaken?
// Question 2: Will Date.prototype.toReadable be removed?
// Question 3: How would you fix this library to be more tree-shaking-friendly?
// Consumer:
import { formatDate } from "./utils"
console.log(formatDate(new Date()))