Tree Shaking:它是什么、死代码消除机制及工具

作者: IT Sectr 发布日期: 2026-05-18 阅读时间: 8 分钟

Tree Shaking — 在应用程序构建阶段删除未使用代码(死代码消除)的机制。Tree Shaking分析ES模块的静态结构,排除未被任何地方导入的导出函数、类和变量。根据Webpack Documentation,正确配置Tree Shaking可以在不改变应用程序功能的情况下将打包文件大小减少30–60%。

要点

  • Tree Shaking — 基于import/export静态分析从ES模块中删除未使用的导出
  • ES模块(import/export)— 唯一支持Tree Shaking的格式;CommonJS不支持
  • Webpack和Rollup — 通过插件支持Tree Shaking的主要打包工具
  • Side effects — 模块中的副作用会阻止Tree Shaking;package.json中的sideEffects: false标志可解决问题
  • Used exports — 在生产模式下分析Webpack导出的使用情况,以精确删除死代码

什么是Tree Shaking?

Tree Shaking(摇树)— 一种代码优化技术,在最终打包文件中排除应用程序中未使用的模块和函数。该术语由Rollup团队于2015年引入,比喻性地描述了这一过程:依赖树被摇动,未使用的分支掉落。与手动优化不同,Tree Shaking在构建阶段自动执行。

Tree Shaking仅适用于ES模块(ECMAScript Modules),其中依赖关系通过import和export静态定义。CommonJS(require/module.exports)不支持Tree Shaking,因为require是动态执行的——打包工具无法预先确定哪些函数真正被使用。现代库(Lodash、Moment.js、RxJS)发布ES版本以支持Tree Shaking。

Rollup:Tree Shaking的先驱

Rollup — 第一个在2015年实现Tree Shaking的打包工具。与Webpack不同,Rollup从一开始就是为ES模块设计的,并执行更积极的死代码删除。Rollup不仅分析单个导出,还分析整个模块:如果模块没有副作用且没有任何导出被使用,Rollup将从打包文件中排除整个模块。

Rollup对于每个千字节都很重要的库和SDK尤其有效。Vue.js框架使用Rollup构建生产版本。React于2020年转向Rollup。对于应用程序,由于更丰富的插件生态系统(热模块替换、代码分割、CSS模块),更常使用Webpack,但在构建库时为了最大化Tree Shaking,Rollup仍然是行业标准。

节省来自Tree Shaking的效果很大程度上取决于项目架构。在带有Ant Design库的React应用程序中,Tree Shaking可以删除多达70%的UI组件代码。在所有导入都是特定且精确的项目中,节省率为5–15%。根据Webpack的研究,平均节省率为打包文件大小的30–40%。

什么是死代码

死代码类型示例Tree Shaking检测
未使用的导出export function unusedHelper()
未使用的导入import { unused } from "lib"
条件死分支if (false) { ... }否(由压缩工具删除)
DCE后未调用的函数function a(){} a() 其中a未被调用部分

Tree Shaking如何工作:模块的静态分析

Tree Shaking机制基于依赖图(dependency graph),打包工具从项目中的所有import/export构建该图。第一阶段,打包工具从入口点(entry point)遍历所有文件并收集模块树。第二阶段,分析每个模块中哪些导出真正被其他模块导入。

对于每个模块,Webpack或Rollup将导出标记为已使用或未使用。未使用的导出将从打包文件中排除。但是,如果至少有一个导出被使用,模块本身仍会保留在打包文件中。完全排除模块只能通过sideEffects标志或模块不包含任何副作用来实现。

示例:Tree Shaking前后

js
// utils.js — 包含函数的模块
export function formatDate(date) {
  return date.toISOString().slice(0, 10);
}

export function formatCurrency(amount) {
  return "$" + amount.toFixed(2);
}

export function slugify(text) {
  return text.toLowerCase().replace(/\s+/g, "-");
}
js
// app.js — 入口点
import { formatDate } from "./utils";

const today = formatDate(new Date());
console.log(today);
js
// Tree Shaking之后 — 打包文件中只有formatDate
function formatDate(date) {
  return date.toISOString().slice(0, 10);
}
const today = formatDate(new Date());
console.log(today);

Tree Shaking从最终打包文件中排除了formatCurrency和slugify,因为它们未在app.js中导入。utils.js模块的大小从3个函数减少到1个。如果utils.js包含副作用(例如全局初始化),Tree Shaking将无法删除即使未使用的导出。

Webpack中的Tree Shaking:配置和优化

Webpack在生产模式下通过TerserPlugin插件包含对Tree Shaking的内置支持。启用Tree Shaking只需两个条件:mode设置为production(mode: "production")且模块使用ES语法(import/export)。Webpack自动标记未使用的导出并将其传递给Terser进行删除。

额外配置usedExports: true在optimization.webpack.config.js中启用了模块内导出使用的详细分析。此选项确定哪些导出真正被使用(used),哪些仅被导出(provided)。usedExports和Terser的组合提供了最大的死代码删除效率。

用于Tree Shaking的Webpack配置

js
// webpack.config.js — Tree Shaking配置
module.exports = {
  mode: "production",
  entry: "./src/app.js",
  output: {
    filename: "bundle.js",
  },
  optimization: {
    usedExports: true,
    minimize: true,
    concatenateModules: true,
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules\/(?!(my-lib)\/).*/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              ["@babel/preset-env", { modules: false }],
            ],
          },
        },
      },
    ],
  },
};

关键参数——modules: false在@babel/preset-env中。Babel默认将ES模块转换为CommonJS,这会扼杀Tree Shaking。modules: false禁止Babel转换import/export,为Webpack保留ES语法。concatenateModules进一步将模块合并到共同作用域中,减少IIFE数量和打包文件大小。

Side effects问题和sideEffects标志

Side effects(副作用)——模块在导入时与导出值无关的操作:全局样式(import "./styles.css")、polyfill(import "core-js/stable")、全局变量初始化或Service Worker注册。如果模块包含副作用,即使没有导出被使用,打包工具也无法安全地将其从打包文件中删除。

sideEffects标志在package.json中告知打包工具包中哪些模块没有副作用。对于所有模块都是纯净(仅导出函数)的包,应设置"sideEffects": false。对于包含CSS或polyfill的包——具有副作用的文件路径数组:"sideEffects": ["*.css"]。没有此标志,Tree Shaking将不会删除即使未使用的函数。

如何在自己的代码中确定副作用

要检查模块是否有副作用,请提出以下问题:此导入是否会执行与导出值无关的任何操作?import "./styles.css"将CSS添加到DOM——这是副作用。import { throttle } from "lodash-es"没有副作用——它只使throttle函数可用。Polyfill(import "core-js/stable")有副作用——它们修改全局原型。

对于自己的模块,建议:将样式和polyfill移至单独的入口点,将纯净工具(无副作用的函数)与有副作用的模块(初始化、日志记录、Service Worker注册)分离。在顶级项目的package.json中,仅当所有模块都是纯净时才设置"sideEffects": false。如果有样式——精确设置"sideEffects": ["*.css"]。

sideEffects配置示例

json
{
  "name": "my-ui-lib",
  "version": "2.1.0",
  "sideEffects": [
    "*.css",
    "polyfills.js"
  ],
  "module": "dist/index.esm.js",
  "main": "dist/index.cjs.js"
}

"sideEffects": ["*.css", "polyfills.js"]表示:所有CSS文件都有副作用(不能删除),polyfills.js也是。包中所有其他JS文件都是纯净的——可以安全地摇树。module字段指向包的ES版本路径,打包工具应使用该版本而非CommonJS版本(main)进行Tree Shaking。

React Native和Metro中的Tree Shaking

React Native与Metro Bundler支持有限版本的Tree Shaking。Metro不像Webpack那样执行完整的已使用导出静态分析(usedExports)。相反,Metro依赖Terser在压缩阶段删除模块的未使用部分。这种方法的效率低于Webpack中的完整Tree Shaking。

为了最大化React Native项目的优化,建议:使用带有ES模块的库(package.json中的module字段),连接babel-plugin-transform-remove-console插件以删除调试代码,并为Terser配置Metro transformer.minifierConfig。此外,Ram Bundle(将打包文件分割成模块)减少了未使用屏幕的加载。

常见问题

为什么Tree Shaking不能与CommonJS一起使用?

CommonJS(require/module.exports)不支持静态分析——require可以在条件和函数内部动态调用。打包工具无法确定模块的哪些部分真正被使用。只有具有静态import/export的ES模块才允许Tree Shaking。

Tree Shaking可以与TypeScript一起使用吗?

TypeScript与Tree Shaking完全兼容,前提是tsconfig.json配置为ES模块:"module": "esnext"。TypeScript编译器必须保留import/export而不转换为CommonJS。带有@babel/preset-typescript和modules: false的Babel也能正确地将ES模块传递给Webpack。

如何检查Tree Shaking是否生效?

Webpack Bundle Analyzer——以交互式图表形式可视化打包文件组成的插件。如果库存在于打包文件中但未使用其功能,则Tree Shaking未生效。也可以分析输出文件:通过grep在打包文件文本中查找未使用的导出。

为什么Lodash默认不会被摇树?

Lodash v4作为CommonJS包分发。对于Tree Shaking,应使用lodash-es——库的ES版本。将import throttle from "lodash/throttle"替换为import { throttle } from "lodash-es",并在Webpack中配置resolve.alias将lodash替换为lodash-es。

Tree Shaking会影响构建时间吗?

Tree Shaking会略微增加构建时间(5–15%),因为它增加了依赖图分析和已使用导出标记的阶段。在开发模式下,Tree Shaking通常为了速度而关闭。在生产模式下,额外的时间被打包文件大小的大幅减少所抵消。

总结

  • Tree Shaking——在构建阶段自动删除未使用的ES导出,将打包文件大小减少30–60%
  • ES模块——唯一支持静态分析的格式;CommonJS不适合Tree Shaking
  • Webpack和Rollup在生产模式下通过usedExports和Terser提供Tree Shaking
  • Side effects会阻止模块删除;package.json中的sideEffects: false标志可解决纯净库的问题
  • Babel必须使用modules: false配置,以避免将ES模块转换为CommonJS
  • React Native Metro具有有限的Tree Shaking,在压缩阶段依赖Terser

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读