Skip to content

tree shaking

压缩可以移除模块内部的无效代码 tree shaking 可以移除模块之间的无效代码

背景

某些模块导出的代码并不一定会被用到

js
// myMath.js
export function add(a, b) {
  console.log("add");
  return a + b;
}

export function sub(a, b) {
  console.log("sub");
  return a - b;
}
js
// index.js
import { add } from "./myMath";
console.log(add(1, 2));

tree shaking 用于移除掉不会用到的导出,上面会移除 sub。

使用

webpack2开始就支持了tree shaking

只要是生产环境,tree shaking自动开启

原理

webpack 会从入口模块出发寻找依赖关系

当解析一个模块时,webpack 会根据 ES6 的模块导入语句来判断,该模块依赖了另一个模块的哪个导出

webpack 之所以选择 ES6 的模块导入语句,是因为 ES6 模块有以下特点:

  1. 导入导出语句只能是顶层语句
  2. import 的模块名只能是字符串常量
  3. import 绑定的变量是不可变的

这些特征都非常有利于分析出稳定的依赖

在具体分析依赖时,webpack 坚持的原则是:保证代码正常运行,然后再尽量 tree shaking

所以,如果你依赖的是一个导出的对象,由于 JS 语言的动态特性,以及webpack还不够智能,为了保证代码正常运行,它不会移除对象中的任何信息

因此,我们在编写代码的时候,尽量

  • 使用export xxx导出,而不使用export default {xxx}导出
  • 使用import {xxx} from "xxx"导入,而不使用import xxx from "xxx"导入

依赖分析完毕后,webpack会根据每个模块每个导出是否被使用,标记其他导出为dead code,然后交给代码压缩工具处理

代码压缩工具最终移除掉那些dead code代码

js
//export导出的会被更好的tree-shaking
export function add(a, b) {
  console.log("add");
  return a + b;
}

export function sub(a, b) {
  console.log("sub");
  return a - b;
}
//默认导出的话,引入的时候,webpack不能更好的确定是否会用到里面具体的哪个,tree-shaking支持就不如上面
export default {
  add: function (a, b) {
    console.log("add");
    return a + b;
  },
  sub: function (a, b) {
    console.log("sub");
    return a - b;
  },
};
js
//更好的tree-shaking
import { add } from "./myMath";
console.log(add(1, 2));

//打包时,会将math模块都打包
import math from "./myMath";
console.log(math);

使用第三方库

某些第三方库可能使用的是commonjs的方式导出,比如lodash

又或者没有提供普通的 ES6 方式导出

对于这些库,tree shaking是无法发挥作用的

因此要寻找这些库的es6版本,好在很多流行但没有使用的ES6的第三方库,都发布了它的ES6版本,比如lodash-es

js
//因为lodash使用的是commonjs,tree-shaking不支持
import { chunk } from "lodash";
console.log(chunk([2, 4, 5, 6], 2));

//换成es版本的lodash就支持了tree-shaking
import { chunk } from "lodash-es";

作用域分析

tree shaking本身并没有完善的作用域分析,可能导致在一些dead code函数中的依赖仍然会被视为依赖

插件webpack-deep-scope-plugin提供了作用域分析,可解决这些问题

js
import { chunk } from "lodash-es"; //打包的时候会tree-shaking

export function add(a, b) {
  console.log("add");
  return a + b;
}

export function myChunk(arr, num) {
  console.log("myChunk");
  return chunk(arr, num);
}
js
//
import { add } from "./myMath";
//实际上,这个项目根本没使用上myChunk,lodash不应该被打包,这个时候webpack就无法识别,需要借助插件
js
const DeepScopePlugin = require("webpack-deep-scope-plugin").default;

module.exports = {
  mode: "production",
  //使用作用域分析插件
  plugins: [new DeepScopePlugin()],
};

注意:这个插件的开发者是个人维护的,随时存在停止维护的风险,在项目中慎重考虑。

副作用问题

webpack 在tree shaking的使用,有一个原则:一定要保证代码正确运行

在满足该原则的基础上,再来决定如何tree shaking

因此,当webpack无法确定某个模块是否有副作用时,它往往将其视为有副作用

因此,某些情况可能并不是我们所想要的

js
//common.js
var n = Math.random();

//index.js
import "./common.js";

虽然我们根本没用有common.js的导出,但webpack担心common.js有副作用,如果去掉会影响某些功能

如果要解决该问题,就需要标记该文件是没有副作用的

package.json中加入sideEffects

json
{
  "sideEffects": false
}

有两种配置方式:

  • false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些 css 文件的导入
  • 数组:设置哪些文件拥有副作用,例如:["!src/common.js"],表示只要不是src/common.js的文件,都有副作用

这种方式我们一般不处理,通常是一些第三方库在它们自己的package.json中标注

json
//package.json
"sideEffects": [
    "!src/common.js"
  ]

css tree shaking

webpack无法对css完成tree shaking,因为csses6没有半毛钱关系

因此对csstree shaking需要其他插件完成

例如:purgecss-webpack-plugin

注意:purgecss-webpack-plugincss module无能为力,因为css module最终生成的 css 名都被 hash 化了,这个插件主要实现是针对名字去和文件做对比

css
body {
  background-color: #666;
}
h1 {
  font-size: 24px;
}
.red {
  color: #f55;
}
js
import "./index.css";

//动态生成的元素
const div = document.createElement("div");
div.innerHTML = "hello inner";
div.className = "red";
document.body.appendChild(div);
js
const MiniCssPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
//
const DeepScopePlugin = require("webpack-deep-scope-plugin").default;
const PurgecssPlugin = require("purgecss-webpack-plugin");
const path = require("path");
const globAll = require("glob-all"); //将global-patterns转换成绝对路径
const srcAbs = path.resolve(__dirname, "src"); //src的绝对路径
const htmlPath = path.resolve(__dirname, "public/index.html"); //html的绝对路径
const paths = globAll.sync([`${srcAbs}**/*.js`, htmlPath]);
console.log(paths);

module.exports = {
  mode: "production",
  //
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssPlugin.loader, "css-loader"],
      },
    ],
  },
  //
  plugins: [
    new MiniCssPlugin(),
    new CleanWebpackPlugin(),
    //
    new DeepScopePlugin(),
    new PurgecssPlugin({
      //指定和哪个文件做对比,然后将css代码tree-shaking,传入的需要是绝对路径
      // paths: [
      //   path.resolve(__dirname, "public/index.html"),
      //   path.resolve(__dirname, "src/index.js"),
      // ],

      //上面需要不停的去对比文件,这样太麻烦了,我们应该用global-patterns  **/*.js
      //而我们需要一个插件把这样的表达式转换为匹配到的路径,这个插件就是global-all
      //这样就将所有的js文件给匹配了一遍,然后进行tree-shaking
      paths,
    }),
  ],
};
json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^3.4.2",
    "glob": "^7.1.6",
    "glob-all": "^3.1.0",
    "lodash": "^4.17.15",
    "lodash-es": "^4.17.15",
    "mini-css-extract-plugin": "^0.9.0",
    "purgecss-webpack-plugin": "^2.1.0",
    "webpack": "^4.41.6",
    "webpack-cli": "^3.3.11",
    "webpack-deep-scope-plugin": "^1.6.2"
  },
  "sideEffects": ["!src/common.js"]
}

MIT License