浅谈Tree Shaking

Tree Shaking 基础知识

什么是 Tree Shaking?

Tree Shaking (摇树优化)是一种通过静态分析消除 JavaScript 中未使用代码(Dead Code)的优化技术。

形象比喻:

复制代码
想象一棵树(代码库):
🌳 整棵树 = 完整的代码库
🍂 枯叶 = 未使用的代码
🤝 摇树 = 构建工具分析
✨ 结果 = 只保留需要的代码

工作原理

1. 静态分析(编译时)

Tree Shaking 依赖于 ES Module (ESM) 的静态结构特性:

javascript 复制代码
// ✅ ESM - 支持 Tree Shaking(静态导入)
import { funcA } from './utils';  // 编译时确定

// ❌ CommonJS - 不支持 Tree Shaking(动态导入)
const { funcA } = require('./utils');  // 运行时确定

关键区别:

特性 ESM CommonJS
导入时机 编译时(静态) 运行时(动态)
结构分析 ✅ 可分析 ❌ 不可分析
Tree Shaking ✅ 支持 ❌ 不支持
语法 import/export require/module.exports

2. 依赖图分析

构建工具如何工作:

javascript 复制代码
// utils.js
export function add(a, b) { return a + b; }      // 被使用 ✅
export function subtract(a, b) { return a - b; } // 未使用 ❌
export function multiply(a, b) { return a * b; } // 未使用 ❌

// main.js
import { add } from './utils';  // 只导入 add
console.log(add(1, 2));

分析过程:

csharp 复制代码
1. 入口分析:main.js 导入了什么?
   → { add } from './utils'

2. 依赖追踪:add 函数依赖什么?
   → 无其他依赖

3. 标记清除:
   ✅ add       → 保留(被使用)
   ❌ subtract  → 删除(未使用)
   ❌ multiply  → 删除(未使用)

4. 最终产物:
   只包含 add 函数

3. 副作用(Side Effects)处理

什么是副作用?

副作用是指除了返回值之外,还会影响外部状态的操作

javascript 复制代码
// ❌ 有副作用的代码
import './polyfill';           // 修改全局对象
import './styles.css';         // 注入样式
console.log('Module loaded');  // 控制台输出
window.myGlobal = 123;         // 修改 window

// ✅ 无副作用的代码(纯函数)
export function add(a, b) {
  return a + b;  // 只返回值,不影响外部
}

常见的副作用:

  • 修改全局变量或对象
  • 修改函数参数
  • 发起网络请求
  • DOM 操作
  • 原型链修改
  • 导入 CSS 文件
  • 执行 IIFE(立即执行函数)
javascript 复制代码
// 修改全局变量或对象
window.myGlobalVar = 'value';
global.config = { theme: 'dark' };

// 修改函数参数
function addProperty(obj) {
  obj.newProp = 'value';
}

// 发起网络请求
fetch('/api/data');

// DOM 操作
document.body.classList.add('loaded');

// 原型链修改
Array.prototype.myMethod = function() {};

// 导入 CSS 文件
import './styles.css';

// 执行 IIFE(立即执行函数)
(function() {
  console.log('初始化');
})();

为什么要标记副作用?

json 复制代码
// package.json
{
  "sideEffects": false  // 告诉打包工具:"所有代码都无副作用"
}

示例:

javascript 复制代码
// utils.js
export function add(a, b) { return a + b; }

// polyfill.js
Array.prototype.includes = function() { ... };  // 有副作用!

// main.js
import { add } from './utils';  // 使用 utils
import './polyfill';            // 不使用,但有副作用

// ❌ 如果 sideEffects: false
// → polyfill.js 被删除(认为无副作用)
// → 导致运行时错误!

// ✅ 如果 sideEffects: ["**/polyfill.js"]
// → polyfill.js 被保留(标记为有副作用)
// → 运行正常

副作用代码与sideEffects配置的关系

  • 副作用代码,这是客观事实,也就是说一段代码是否是副作用代码,已经是确定的。

  • package.json sideEffects,文件级别配置,用来人为声明文件是否"可能有副作用",从而决定未引用的文件能否被整体删除。

    • 默认为true,则编译器不敢假设文件无副作用,所以不会删除未引用的文件。属于保守策略。
    • 如果配置为false,则编译器会把所有文件都视为无副作用文件,只要文件没有被import,就可以被整个删除。属于激进策略
    • 如果配置为文件数组,则配置的文件会认为有副作用,即使文件没被引用也会被保留和打包;其他文件认为没有副作用代码,如果文件没被引用则编译会被移除不会被打包。比如在库中,一般CSS文件和polyfill文件就要被包含在内,比如:["*.css", "src/polyfill.js"]

Tree Shaking 的前提条件

✅ 必须满足的条件

  1. 使用 ESM 模块格式
json 复制代码
// package.json
{
  "type": "module",
  "module": "dist/esm/index.js"
}
  1. 构建工具支持

    • Webpack 4+
    • Rollup(原生支持)
    • Vite(基于 Rollup)
    • esbuild
  2. 正确配置 sideEffects

json 复制代码
{
  "sideEffects": false,           // 无副作用
  // 或
  "sideEffects": ["**/*.css"]     // 只有 CSS 有副作用
}
  1. 避免动态导入(在需要 Tree Shaking 的地方)
javascript 复制代码
// ❌ 动态导入 - 无法 Tree Shaking
const moduleName = 'utils';
import(`./${moduleName}`);

// ✅ 静态导入 - 可以 Tree Shaking
import { func } from './utils';

常见阻碍 Tree Shaking 的情况

1. 使用 CommonJS

javascript 复制代码
// ❌ 不支持 Tree Shaking
module.exports = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
};

// ✅ 支持 Tree Shaking
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

2. 默认导出整个对象

javascript 复制代码
// ❌ 难以 Tree Shaking
export default {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
};

// 用户使用
import utils from './utils';
utils.add(1, 2);  // 整个对象都被导入

// ✅ 易于 Tree Shaking
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// 用户使用
import { add } from './utils';  // 只导入需要的

3. 副作用未正确标记

javascript 复制代码
// styles.css
.button { color: red; }

// Component.js
import './styles.css';  // 副作用:注入样式
export function Component() { ... }

// package.json
{
  "sideEffects": false  // ❌ 错误!CSS 会被删除
}

// ✅ 正确配置
{
  "sideEffects": ["**/*.css"]  // CSS 不会被删除
}

4. 类和构造函数

javascript 复制代码
// ❌ 类的方法可能无法 Tree Shaking
class Utils {
  add(a, b) { return a + b; }
  subtract(a, b) { return a - b; }
}

// 用户使用
import { Utils } from './utils';
const u = new Utils();
u.add(1, 2);  // 整个类都被导入

// ✅ 更好的方式(纯函数)
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }

Tree Shaking 实战示例

示例 1:基础 Tree Shaking

库代码:

javascript 复制代码
// math-lib/index.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }

用户代码:

csharp 复制代码
// app.js
import { add } from 'math-lib';

console.log(add(1, 2));

打包结果:

javascript 复制代码
// bundle.js(简化)
function add(a, b) { return a + b; }
console.log(add(1, 2));

// ✅ subtract、multiply、divide 被删除

示例 2:组件库 Tree Shaking

库代码:

javascript 复制代码
// ui-lib/index.js
export { Button } from './Button';
export { Input } from './Input';
export { Select } from './Select';
export { Modal } from './Modal';

// ui-lib/Button.js
import './Button.css';  // 副作用
export function Button() { ... }

// package.json
{
  "sideEffects": ["**/*.css"]  // 保护 CSS
}

用户代码:

javascript 复制代码
// app.js
import { Button } from 'ui-lib';

Button();

打包结果:

javascript 复制代码
// bundle.js
import './Button.css';  // ✅ CSS 被保留(标记为副作用)
function Button() { ... }
Button();

// ✅ Input、Select、Modal 及其 CSS 都被删除

Tree Shaking 效果验证

方法 1:分析打包结果

r 复制代码
# Webpack
npm run build -- --analyze

# Rollup
rollup -c --plugin visualizer

# Vite
vite build --mode production
# 查看 dist/stats.html

方法 2:手动检查

arduino 复制代码
// 1. 构建生产版本
npm run build

// 2. 搜索未使用的代码
grep -r "unusedFunction" dist/

// 3. 如果找到 → Tree Shaking 失败
// 如果未找到 → Tree Shaking 成功

方法 3:对比体积

javascript 复制代码
// 测试 1:引入所有
import * as All from 'my-lib';
// 打包体积:100 KB

// 测试 2:只引入一个
import { Button } from 'my-lib';
// 打包体积:20 KB

// ✅ Tree Shaking 有效:减少 80%

为库开发者的 Tree Shaking 检查清单

✅ 构建配置

  • 使用 ESM 输出格式(format: 'es'
  • 配置 preserveModules: true(保留模块结构)
  • 设置 package.jsonmodule 字段
  • 设置 package.jsonsideEffects 字段

✅ 代码规范

  • 使用 export 而非 export default {}
  • 避免在模块顶层执行副作用代码
  • 纯函数优于类(函数更容易 Tree Shaking)
  • 将 CSS 导入标记为副作用

✅ 文档说明

  • 在 README 中说明支持 Tree Shaking
  • 提供按需引入的示例
  • 标注哪些文件有副作用

常见误区

❌ 误区 1:"压缩 = Tree Shaking"

javascript 复制代码
// 压缩(Minify):缩短变量名、删除空格
function add(a,b){return a+b}

// Tree Shaking:删除未使用的代码
// 如果 add 没被用,整个函数被删除

// 两者不同!

❌ 误区 2:"webpack 自动 Tree Shaking"

javascript 复制代码
// ❌ 不会自动 Tree Shaking(CommonJS)
const lib = require('my-lib');

// ✅ 才能 Tree Shaking(ESM)
import { func } from 'my-lib';

❌ 误区 3:"设置 sideEffects: false 就够了"

arduino 复制代码
// package.json
{ "sideEffects": false }

// ❌ 但代码中有副作用
import './polyfill';  // 会被删除!
import './styles.css'; // 会被删除!

// ✅ 正确配置
{ "sideEffects": ["**/polyfill.js", "**/*.css"] }

小结

Tree Shaking 核心要点:

  1. 基于 ESM 的静态分析
  2. 编译时 确定依赖关系
  3. 删除 未使用的代码
  4. 保留 有副作用的代码
  5. 需要 构建工具、模块格式、配置三者配合

记忆口诀:

arduino 复制代码
ESM 是前提(import/export)
静态分析为基础(编译时确定)
标记副作用要清楚(sideEffects)
保留结构才有效(preserveModules)

附录

相关推荐
EricChao3 天前
你的图标还在用 PNG?看完这篇你就会换成 iconfont
前端工程化
一川_9 天前
从 Vue 构建错误到深度解析:::v-deep 引发的 CSS 压缩危机
css·前端工程化
answerball14 天前
Webpack:从构建流程到性能优化的深度探索
javascript·webpack·前端工程化
kuromiluu16 天前
前端工程化效率神器:pnpm 从入门到精通
前端工程化
云枫晖17 天前
前端工程化实战:手把手教你构建项目脚手架
前端·前端工程化
Zzzzzxl_18 天前
互联网大厂前端面试实录:HTML5、ES6、Vue/React、工程化与性能优化全覆盖
性能优化·vue·es6·react·html5·前端面试·前端工程化
井柏然20 天前
重识 alias —— npm包开发的神器
前端·javascript·前端工程化
trsoliu1 个月前
2025前端AI开发实战范式:RAG+私有库落地指南
前端框架·前端工程化
白水清风1 个月前
Vue3之响应式系统
vue.js·面试·前端工程化