likes
comments
collection
share

webpack tree-shaking解析

作者站长头像
站长
· 阅读数 9

生产环境、开发环境 treeShaking

初始化工作

npm init -y 初始化 package.json 文件,然后安装 webpack 包

npm install -D webpack webpack-cli

根目录下,创建 webpack.config.js

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

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "bundle.js",
  },
  mode: 'production',
}

然后在 src/treeShaking 文件夹下创建 a.js

// ./src/treeShaking/a.js
export const a = () => {
  console.log('a');
}

export const b = () => {
  console.log('b');
}

然后在 src 文件夹下创建入口文件 index.js

import { a, b } from "./treeShaking/a";

a();

const unused = '死代码1'

if (false) {
  console.log('死代码2')
}

index.js 文件中,b是引入了但没有使用的模块变量,所以它是死代码,unused 是定义了但没有用到的变量,也是死代码,false判断条件里面的 console.log也不会被执行,所以也是死代码

生产环境直接打包

上面我们已经配置了 webpack 的 mode 是 production,直接打包看效果

package.json 里面配置命令

 "scripts": {
    "build": "webpack --config ./webpack/webpack.treeShaking.js",
  },

然后 npm run build 打包后的 dist/bundle.js 文件:

webpack tree-shaking解析

我们发现只有 a 被打包了,其他死代码都被 treeshaking 掉了,也就是说,在 production 模式下,默认开启了 treeshaking。那 treeshaking 是怎么工作的呢?

开发环境观察treeShaking

修改一下 webpack 的配置

const path = require("path");

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "bundle.js",
  },
  mode: 'development',
  devtool: 'source-map',
  optimization: {
    //开发环境观察 tree-shaking 配置,打包后会标注 export、import 的变量是否用到了
    usedExports: true,
  }
}

这个时候我们再打包观察 bundle.js 文件

webpack tree-shaking解析

webpack tree-shaking解析

我们发现导出的 exports 对象上,只挂载了 a 函数,b函数用注释 /* unused harmony export b */标记了,if判断里面的逻辑被删除了,unused虽然没有使用但还是保存了下来。注意,此时只做了标记操作,删除死代码的逻辑是在 Taser-webpack-plugin 里面做的

然后我们再修改一下 webpack 的配置,开启 minimize: true手动 tree-shaking

const path = require("path");

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "bundle.js",
  },
  // mode: 'production',
  mode: 'development',
  devtool: 'source-map',
  optimization: {
    //开发环境观察 tree-shaking 配置,打包后会标注 export、import 的变量是否用到了
    usedExports: true,
    //删除被标记,也就是没有用到的死代码
    minimize: true
  }
}

再打包

webpack tree-shaking解析

发现只有 a 函数被挂载到 exports 对象上,bunusedif 都被删除了

treeShaking流程

可以概括为三个步骤:

  • 收集模块导出变量,并将其记录到 ModuleGraph 变量中
  • 遍历 ModuleGraph 变量,标记当前模块导出变量是否被使用上,没被使用就通过 /* unused harmony export b */ 标记
  • 打包时,通过 Taser-webpack-plugin 删除被 /* unused harmony export b */ 标记的模块导出变量

CommonJS 可以 treeShaking 吗

如果 index.js 里面是这样的代码

// ./src/index.js
if (Math.random() > 0.5) {
    require('a').then((res) => console.log(res))
} else {
    require('b').then((res) => console.log(res))
}

那现在我们就不能判断到底是 a 用到了还是 b 用到了,因为 require 是动态导入,对于 CommonJs, 在运行时才能知道到底哪些模块变量用到了哪些没用到

而对于 ESModule,它通过 exportimport 静态导入导出的语法,在 编译 时,就知道了哪些模块变量用到了哪些没用到,所以 treeShaking 只对 ES module 形式导入导出的模块生效

sideEffects 副作用

某些情况下, treeShaking 会失效,比如下面的代码

// ./src/treeShaking/a.js
window.a = '全局a'

// ./src/index.js
import './treeShaking/a'

console.log('bbb');

然后打包,观察 bundle.js 文件

webpack tree-shaking解析

按理说,我在 window 对象上挂了一个属性 a,但是我并没有使用 window.a,应该会被 treeShaking 掉,但是打包后的结果并没有删除掉

原因是 Webpack 的 Tree Shaking 逻辑停留在代码 静态分析 层面,只是浅显地判断:

  • 模块导出变量是否被其它模块引用
  • 引用模块的主体代码中有没有出现这个变量

更深层次的原因则是产生意料之外的副作用

这个时候,可以通过配置 package.jsonsideEffects 来消除副作用

它的配置选项如下:

  • true:所有文件都有副作用不可以 tree-shaking
  • false:所有文件没有副作用可以 tree-shaking
  • 数组只有数组中的文件有副作用不可以被 tree-shaking,其它的都可以 tree-shaking

然后我们配置了 sideEffects: false 之后,标识所有文件都没有副作用,都要做 treeShaking,然后重新打包

webpack tree-shaking解析

结语

以上就是本篇 treeShaking 的内容,如果对源码有兴趣,可以去看这篇文章 # Webpack 原理系列九:Tree-Shaking 实现原理