Webpack系列-Tree Shaking 简介

966 阅读4分钟

简介

tree shaking 翻译过来的意思是摇掉树上的黄叶。

tree shaking 用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import 和 export。

因为ES6模块的出现,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,

例如下面的demo

// webpack.config.js
const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    mode: 'development',
    optimization: {
        usedExports: true
    }
}
// utils.js
const func1 = function(value) {
    console.log(value)
}

const func2 = function(value) {
    return value === null
}

export {
    func1,
    func2
}
// index.js

import { func2 } from './utils.js'
func2(11)

在index.js中引入了func.js中的func2,并没有引入func1,使用tree shaking打包完的代码就会去除 func2,只保留func1。但是webpack只能做比较简单去除。

如果把代码加以改变,如下这样

// utils.js
import lodash from 'lodash-es'
const func1 = function(value) {
    return lodash.isArray(value)
}

const func2 = function(value) {
    return value === null
}

export {
    func1,
    func2
}

上面的代码webpack依然会去除func2,但是webpack检查的时候发现utils.js中的确用到了lodash,所以不会把lodash去掉,实际上,我们根本没用到它。 插件

webpack-deep-scope-analysis-plugin c

webpack-deep-scope-analysis-plugin 插件就可以解决这种判断

配置webpack.config.js

const WebpackDeepScopeAnalysisPlugin = require('webpack-deep-scope-plugin').default;

module.exports = {
    ...,
    plugins: [
        ...,
        new WebpackDeepScopeAnalysisPlugin(),
    ],
}

插件原理

那么这个插件是怎么去解决这个问题的呢?这里根据原作者在Medium上写的文章,简单介绍一下他的做法。

webpack的原理,其实就是遍历所有的模块,把它们打包成一个文件,在这个过程中,它就知道哪些export的模块有被使用到。那我们同样也可以遍历所有的scope(作用域),简化没有用到的scope,最后只留下我们需要的。

副作用

仅仅因为 Webpack 看不到一段正在使用的代码,并不意味着它可以安全地进行 tree-shaking。有些模块导入,只要被引入,就会对应用程序产生重要的影响。一个很好的例子就是全局样式表,或者设置全局配置的JavaScript 文件。

Webpack 认为这样的文件有“副作用”。具有副作用的文件不应该做 tree-shaking,因为这将破坏整个应用程序。Webpack 的设计者清楚地认识到不知道哪些文件有副作用的情况下打包代码的风险,因此默认地将所有代码视为有副作用。这可以保护你免于删除必要的文件,但这意味着 Webpack 的默认行为实际上是不进行 tree-shaking。

幸运的是,我们可以配置我们的项目,告诉 Webpack 它是没有副作用的,可以进行 tree-shaking。

如何告诉 Webpack 你的代码无副作用

package.json 有一个特殊的属性 sideEffects,就是为此而存在的。它有三个可能的值:

  • true 是默认值,如果不指定其他值的话。这意味着所有的文件都有副作用,也就是没有一个文件可以 tree-shaking。
  • false 告诉 Webpack 没有文件有副作用,所有文件都可以 tree-shaking。
  • 第三个值 […] 是文件路径数组。它告诉 webpack,除了数组中包含的文件外,你的任何文件都没有副作用。因此,除了指定的文件之外,其他文件都可以安全地进行 tree-shaking。
// 所有文件都有副作用,全都不可 tree-shaking
{
    "sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking
{
    "sideEffects": false
}
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
{
    "sideEffects": [
        "./src/file1.js",
        "./src/file2.js"
    ]
}

注意

所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:

{
    "name": "your-project",
    "sideEffects": [
        "./src/some-side-effectful-file.js",
        "*.css"
    ]
}

最后,还可以在 module.rules 配置选项 中设置 "sideEffects"。

结论

想要使用 tree shaking 必须注意以下

  • 使用 ES2015 模块语法(即 import 和 export)。
  • 确保没有 compiler 将 ES2015 模块语法转换为 CommonJS 模块(这也是流行的 Babel preset 中 @babel/preset-env 的默认行为)
  • 在项目 package.json 文件中,添加一个 "sideEffects" 属性。
  • 通过将 mode 选项设置为 production,启用 minification(代码压缩) 和 tree shaking。

参考文章