Tree Shaking

文章目录

  • 前言
  • [一、什么是 Tree Shaking](#一、什么是 Tree Shaking)
    • [1.1 定义](#1.1 定义)
    • [1.2 为什么叫"摇树"](#1.2 为什么叫"摇树")
  • [二、ESM 静态分析](#二、ESM 静态分析)
    • [2.1 ESM 的静态特性](#2.1 ESM 的静态特性)
    • [2.2 为什么 ESM 能 Tree Shaking](#2.2 为什么 ESM 能 Tree Shaking)
  • [三、CJS 为何难摇树](#三、CJS 为何难摇树)
    • [3.1 CommonJS 的动态特性](#3.1 CommonJS 的动态特性)
    • [3.2 CJS 的 Tree Shaking 限制](#3.2 CJS 的 Tree Shaking 限制)
    • [3.3 解决方案](#3.3 解决方案)
  • [四、sideEffects 配置](#四、sideEffects 配置)
    • [4.1 定义](#4.1 定义)
    • [4.2 什么是副作用](#4.2 什么是副作用)
    • [4.3 副作用标记错误会怎样](#4.3 副作用标记错误会怎样)
  • [五、Webpack 配置](#五、Webpack 配置)
    • [5.1 启用 Tree Shaking](#5.1 启用 Tree Shaking)
    • [5.2 两步过程](#5.2 两步过程)
  • 六、易混淆点
  • 七、思考与练习
  • 总结

前言

Tree Shaking 是现代构建工具的核心优化技术,通过静态分析移除未使用的代码,减小打包体积。本篇会讲清楚:

  • Tree Shaking 的原理
  • ESM 静态分析的优势
  • sideEffects 配置
  • CJS 为何难摇树

一、什么是 Tree Shaking

1.1 定义

Tree Shaking 是指通过静态分析,移除项目中未被引用的代码(Dead Code),从而减小最终打包体积。

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

export function subtract(a, b) {
  return a - b
}

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

// Tree Shaking 后:subtract 函数被移除

1.2 为什么叫"摇树"

这个术语来自 ES6 模块的静态结构特性------就像摇树一样,把没有被引用的"枯叶"(未使用的导出)摇落。


二、ESM 静态分析

2.1 ESM 的静态特性

ES6 模块(ESM)的 import / export静态声明,在编译时就能确定依赖关系。

javascript 复制代码
// ✅ ESM:静态导入,编译时确定
import { add } from './utils'

// ❌ 不能这样写(动态导入无法静态分析)
if (condition) {
  import { add } from './utils'
}

// ✅ 动态导入(但这是按需加载,不是 Tree Shaking)
const module = await import('./utils')

2.2 为什么 ESM 能 Tree Shaking

javascript 复制代码
// ESM 的 import 是声明式的
import { add } from './utils'

// Webpack 可以在编译时分析出:
// 1. utils.js 导出了 add 和 subtract
// 2. main.js 只使用了 add
// 3. 因此可以安全移除 subtract

三、CJS 为何难摇树

3.1 CommonJS 的动态特性

CommonJS(CJS)的 require动态执行的,无法在编译时确定依赖关系。

javascript 复制代码
// CJS:require 是函数调用,运行时执行
const utils = require('./utils')

// 无法在编译时确定:
// 1. utils 是否被使用
// 2. utils 的哪些属性被使用
// 3. require 的路径是否是动态的

// 动态路径
const moduleName = getModuleName()
const module = require(moduleName)

3.2 CJS 的 Tree Shaking 限制

javascript 复制代码
// CJS 导出
module.exports = {
  add: function(a, b) { return a + b },
  subtract: function(a, b) { return a - b }
}

// CJS 导入
const utils = require('./utils')
console.log(utils.add(1, 2))

// 问题:无法确定 utils.subtract 是否被使用
// 因为 utils 是一个对象,属性访问是动态的

3.3 解决方案

javascript 复制代码
// 方案 1:使用 ESM 语法
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b }

// 方案 2:使用具名导出(而不是 default 导出整个对象)
// ❌ 不利于 Tree Shaking
export default { add, subtract }

// ✅ 有利于 Tree Shaking
export { add, subtract }

四、sideEffects 配置

4.1 定义

sideEffects 告诉 Webpack 哪些文件有副作用,哪些没有。没有副作用的文件如果未被使用,可以安全移除。

javascript 复制代码
// package.json
{
  "sideEffects": false  // 所有文件都没有副作用
}

// 或指定有副作用的文件
{
  "sideEffects": [
    "*.css",
    "*.polyfill.js"
  ]
}

4.2 什么是副作用

javascript 复制代码
// 有副作用的代码
let globalVar = 0

export function increment() {
  globalVar++
  return globalVar
}

// 这个模块修改了全局变量,不能被移除
// 即使 increment 函数没有被导入使用

// 无副作用的代码
export function add(a, b) {
  return a + b
}

// 这个模块是纯函数,可以安全移除

4.3 副作用标记错误会怎样

javascript 复制代码
// 错误:将有副作用的模块标记为无副作用
{
  "sideEffects": false
}

// 代码:
import './polyfill'  // 修改了全局对象
import { add } from './utils'

// 结果:polyfill 被错误移除,导致兼容性问题

五、Webpack 配置

5.1 启用 Tree Shaking

javascript 复制代码
// webpack.config.js
module.exports = {
  mode: 'production',  // 生产模式自动启用 Tree Shaking
  optimization: {
    usedExports: true,  // 标记未使用的导出
    minimize: true      // 压缩时移除未使用的代码
  }
}

5.2 两步过程

复制代码
1. usedExports:标记未使用的导出
   - 分析模块图
   - 标记哪些导出被使用,哪些没有

2. minimize(压缩):移除未使用的代码
   - Terser 等压缩工具识别标记
   - 移除未使用的导出

六、易混淆点

  1. Tree Shaking 只对 ESM 有效 :CJS 的 require 是动态执行的,无法在编译时静态分析。
  2. sideEffects: false 的含义:告诉 Webpack 所有模块都没有副作用,未使用的模块可以安全移除。
  3. 副作用标记错误:将有副作用的模块标记为无副作用,会导致全局修改、polyfill 等被错误移除。
  4. production 模式:Webpack 的 production 模式自动启用 Tree Shaking,无需手动配置。
  5. default 导出不利于 Tree Shakingexport default { add, subtract } 无法分析单个函数是否被使用。

七、思考与练习

1. 为什么 ESM 可以 Tree Shaking 而 CJS 不行?

解析:

  • ESMimport / export 是静态声明,编译时确定依赖关系
  • CJSrequire 是函数调用,运行时执行,无法静态分析

2. 什么是副作用?sideEffects: false 的作用是什么?

解析:

  • 副作用:模块执行时会影响全局状态(如修改全局变量、设置原型链等)
  • sideEffects: false:告诉 Webpack 所有模块都没有副作用,未使用的模块可以安全移除

3. 错误标记 sideEffects 会有什么后果?

解析:将有副作用的模块标记为无副作用,会导致:

  • polyfill 被移除,兼容性问题
  • 全局修改被移除,功能异常
  • 样式文件被移除,页面样式丢失

4. 如何让 CJS 模块也能 Tree Shaking?

解析:

  • 方案 1:改用 ESM 语法
  • 方案 2:使用具名导出(而不是 default 导出整个对象)
  • 方案 3:使用工具将 CJS 转换为 ESM

5. Tree Shaking 的两步过程是什么?

解析:

  1. usedExports:标记未使用的导出(分析模块图)
  2. minimize:压缩时移除未使用的代码(Terser 识别标记)

6. export default { add, subtract } 为什么不利于 Tree Shaking?

解析:default 导出是一个整体对象,Webpack 无法分析单个属性(add、subtract)是否被使用。应该使用具名导出:

javascript 复制代码
// ❌ 不利于 Tree Shaking
export default { add, subtract }

// ✅ 有利于 Tree Shaking
export { add, subtract }

总结

  • Tree Shaking 通过静态分析移除未使用的代码,减小打包体积
  • ESM 静态特性import / export 是声明式的,编译时确定依赖
  • CJS 动态特性require 是函数调用,无法静态分析
  • sideEffects:标记有副作用的文件,避免错误移除
  • 两步过程usedExports 标记 + minimize 移除
相关推荐
lichenyang4531 小时前
给 ArkTS 应用做一个内置的「Network 面板」:实时看清 SSE 每一帧和最后那张卡片
前端
倾颜1 小时前
从手写 Runner 到 LangGraph:受控 Agent 接入 LangGraph
前端·后端·langchain
UXbot1 小时前
AI网页开发工具能替代工具吗?5大平台对比
前端·人工智能·低代码·ui·原型模式·web app
wuhen_n1 小时前
从零到一!前端搭建本地轻量化 RAG 问答系统
前端·langchain·ai编程
落日漫游2 小时前
代码报错难排查?借助Gemini快速修复
前端
niconicoC2 小时前
让 Three.js 场景更真实:我用高斯泼溅和 SparkJS 做了一个可交互的 3D Demo
前端·webgl
Darling噜啦啦2 小时前
JavaScript 数组深度解析:从纯函数到二维数组陷阱,一文吃透前端数据结构核心
前端·javascript·数据结构
万少2 小时前
一封邮件,让我重新打开了搁置半年的鸿蒙应用
前端·javascript·后端
wjj不想说话2 小时前
你的小程序活动页,可能已经成了后台配置的杂物间
前端