Tree Shaking — 在应用程序构建阶段删除未使用代码(死代码消除)的机制。Tree Shaking分析ES模块的静态结构,排除未被任何地方导入的导出函数、类和变量。根据Webpack Documentation,正确配置Tree Shaking可以在不改变应用程序功能的情况下将打包文件大小减少30–60%。
要点
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 — 第一个在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机制基于依赖图(dependency graph),打包工具从项目中的所有import/export构建该图。第一阶段,打包工具从入口点(entry point)遍历所有文件并收集模块树。第二阶段,分析每个模块中哪些导出真正被其他模块导入。
对于每个模块,Webpack或Rollup将导出标记为已使用或未使用。未使用的导出将从打包文件中排除。但是,如果至少有一个导出被使用,模块本身仍会保留在打包文件中。完全排除模块只能通过sideEffects标志或模块不包含任何副作用来实现。
// 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, "-");
}// app.js — 入口点
import { formatDate } from "./utils";
const today = formatDate(new Date());
console.log(today);// 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在生产模式下通过TerserPlugin插件包含对Tree Shaking的内置支持。启用Tree Shaking只需两个条件:mode设置为production(mode: "production")且模块使用ES语法(import/export)。Webpack自动标记未使用的导出并将其传递给Terser进行删除。
额外配置usedExports: true在optimization.webpack.config.js中启用了模块内导出使用的详细分析。此选项确定哪些导出真正被使用(used),哪些仅被导出(provided)。usedExports和Terser的组合提供了最大的死代码删除效率。
// 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(副作用)——模块在导入时与导出值无关的操作:全局样式(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"]。
{
"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 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(将打包文件分割成模块)减少了未使用屏幕的加载。
常见问题
CommonJS(require/module.exports)不支持静态分析——require可以在条件和函数内部动态调用。打包工具无法确定模块的哪些部分真正被使用。只有具有静态import/export的ES模块才允许Tree Shaking。
TypeScript与Tree Shaking完全兼容,前提是tsconfig.json配置为ES模块:"module": "esnext"。TypeScript编译器必须保留import/export而不转换为CommonJS。带有@babel/preset-typescript和modules: false的Babel也能正确地将ES模块传递给Webpack。
Webpack Bundle Analyzer——以交互式图表形式可视化打包文件组成的插件。如果库存在于打包文件中但未使用其功能,则Tree Shaking未生效。也可以分析输出文件:通过grep在打包文件文本中查找未使用的导出。
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会略微增加构建时间(5–15%),因为它增加了依赖图分析和已使用导出标记的阶段。在开发模式下,Tree Shaking通常为了速度而关闭。在生产模式下,额外的时间被打包文件大小的大幅减少所抵消。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。