Tree Shaking 是一种优化技术,用于移除模块之间的无效代码。与代码压缩不同,代码压缩主要是移除模块内部的无效代码,而 Tree Shaking 则是移除模块之间未使用的导出。这可以显著减少最终打包文件的体积,提高加载速度。
1. 背景
某些模块导出的代码并不一定会被用到。例如:
// myMath.js
export function add(a, b) {
console.log("add");
return a + b;
}
export function sub(a, b) {
console.log("sub");
return a - b;
}
// index.js
import { add } from "./myMath";
console.log(add(1, 2));
在这个例子中,sub 函数并没有被使用,Tree Shaking 可以移除掉未使用的 sub 函数。
2. 使用
从 Webpack 2 开始,就支持了 Tree Shaking。只要在生产环境中,Tree Shaking 会自动开启。
3. 原理
Webpack 会从入口模块出发,寻找依赖关系。当解析一个模块时,Webpack 会根据 ES6 的模块导入语句来判断,该模块依赖了另一个模块的哪个导出。Webpack 选择 ES6 的模块导入语句,是因为 ES6 模块有以下特点:
- 导入导出语句只能是顶层语句:不能放在条件语句或循环中。
- import 的模块名只能是字符串常量:不能是变量。
- import 绑定的变量是不可变的:一旦导入,不能重新赋值。
这些特征非常适合进行静态分析,从而确定哪些代码是未使用的。
在具体分析依赖时,Webpack 坚持的原则是:保证代码正常运行,然后再尽量 Tree Shaking。因此,如果你依赖的是一个导出的对象,由于 JavaScript 语言的动态特性,以及 Webpack 的智能限制,为了保证代码正常运行,它不会移除对象中的任何信息。
因此,我们在编写代码时,尽量:
- 使用
export xxx导出,而不使用export default {xxx}导出 - 使用
import {xxx} from "xxx"导入,而不使用import xxx from "xxx"导入
依赖分析完毕后,Webpack 会根据每个模块每个导出是否被使用,标记其他导出为 dead code,然后交给代码压缩工具处理。代码压缩工具最终移除掉那些 dead code 代码。
4. 使用第三方库
某些第三方库可能使用的是 CommonJS 方式导出,比如 lodash,或者没有提供普通的 ES6 方式导出。对于这些库,Tree Shaking 是无法发挥作用的。因此,要寻找这些库的 ES6 版本。好在很多流行但没有使用 ES6 的第三方库,都发布了它们的 ES6 版本,比如 lodash-es。
5. 作用域分析
Tree Shaking 本身并没有完善的作用域分析,可能导致在一些 dead code 函数中的依赖仍然被视为依赖。插件 webpack-deep-scope-plugin 提供了作用域分析,可以解决这些问题。
6. 副作用问题
Webpack 在 Tree Shaking 的使用中,有一个原则:一定要保证代码正确运行。在满足该原则的基础上,再来决定如何 Tree Shaking。因此,当 Webpack 无法确定某个模块是否有副作用时,它往往将其视为有副作用。这可能导致某些情况并不是我们所想要的。
例如:
// common.js
var n = Math.random();
// index.js
import "./common.js";
虽然我们根本没有使用 common.js 的导出,但 Webpack 担心 common.js 有副作用,如果去掉会影响某些功能。如果要解决该问题,就需要标记该文件是没有副作用的。
副作用:函数运行过程中,可能会对外部环境造成影响的功能
在 package.json 中加入 sideEffects:
{
"sideEffects": false
}
有两种配置方式:
- false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些 CSS 文件的导入。
- 数组:设置哪些文件拥有副作用,例如:
["!src/common.js"],表示只要不是src/common.js的文件,都有副作用。
这种方式通常由第三方库在其 package.json 中标注。
7. CSS Tree Shaking
Webpack 无法对 CSS 完成 Tree Shaking,因为 CSS 与 ES6 模块没有直接关系。因此,对 CSS 的 Tree Shaking 需要其他插件完成。例如,purgecss-webpack-plugin 可以实现 CSS 的 Tree Shaking。
const PurgecssPlugin = require('purgecss-webpack-plugin');
const path = require('path');
module.exports = {
plugins: [
new PurgecssPlugin({
paths: [
path.resolve(__dirname, './src/**/*.html'),
path.resolve(__dirname, './src/**/*.js'),
path.resolve(__dirname, './src/**/*.vue')
],
safelist: {
standard: ['body', 'html'],
deep: [/^btn-/],
greedy: [/^nav-/]
}
})
]
};
注意:purgecss-webpack-plugin 对 CSS Modules 无能为力。
8. 总结
通过配置 Tree Shaking,可以显著减少最终打包文件的体积,提高加载速度。