webpack5中的tree-shaking 升级的那些事

216 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

简介

webpack官方有标准的说法:Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination

webpack5中有一项的重大变更就是构建优化,其中就有针对tree-shaking做出的调整

webpack5以前的tree-shaking

webpack5 以前,tree-shaking 比较简单,主要是找import进来的变量是否在这个模块内出现过,出现过则不剔除,不出现过则剔除。并且用于 esModule 中

webpack5 更新tree-shaking

嵌套的 tree-shaking

Webpack 现在能够跟踪对导出的嵌套属性的访问。这可以改善重新导出命名空间对象时的 Tree Shaking(清除未使用的导出和混淆导出)。

// inner.js
export const a = 1;
export const b = 2;

// module.js
export * as inner from './inner';
// 或 import * as inner from './inner'; export { inner };

// user.js
import * as module from './module';
console.log(module.inner.a);

在这个例子中,可以在生产模式下删除导出的b

内部模块 tree-shaking

Webpack 4 没有分析模块的导出和引用之间的依赖关系。webpack 5 有一个新的选项 optimization.innerGraph,在生产模式下是默认启用的,它可以对模块中的标志进行分析,找出导出和引用之间的依赖关系。

在这样的模块中:

import { something } from './something';

function usingSomething() {
  return something;
}

export function test() {
  return usingSomething();
}

内部依赖图算法会找出 something 只有在使用 test 导出时才会使用。这允许将更多的出口标记为未使用,并从代码包中省略更多的代码。

当设置"sideEffects": false时,可以省略更多的模块。在这个例子中,当 test 导出未被使用时,./something 将被省略。

要获得未使用的导出信息,需要使用 optimization.unusedExports。要删除无副作用的模块,需要使用optimization.sideEffects

可以分析以下标记。

  • 函数声明

  • 类声明

  • 默认导出export default 或定义变量以下的:

    • 函数表达式
    • 类表达式
    • 顺序表达式
    • /*#__PURE__*/ 表达式
    • 局部变量
    • 引入的捆绑(bindings)

使用 eval() 将为一个模块放弃这个优化,因为经过 eval 的代码可以引用范围内的任何标记。

这种优化也被称为深度范围分析。 webpack5,可以进行根据作用域之间的关系进行优化。比如: a.js 中到处了两个方法 a 和 b,在 index.js 中引入了 a.js 到处的 a 方法,没有引用 b 方法。那么 webpack4 打包出来的结果包含了 index.js 和 a.js 的内容,包含了没有用到的 b 方法。但是 webpack5 的 treeshaking,会进行作用域分析,打包结果只有 index 和 a 文件中的 a 方法,没有用到的 b 方法是不会被打包进来的。

CommonJs Tree Shaking

Webpack 曾经不进行对 CommonJs 导出和 require() 调用时的导出使用分析。

Webpack 5 增加了对一些 CommonJs 构造的支持,允许消除未使用的 CommonJs 导出,并从 require() 调用中跟踪引用的导出名称。

支持以下构造:

  • exports|this|module.exports.xxx = ...

  • exports|this|module.exports = require("...") (reexport)

  • exports|this|module.exports.xxx = require("...").xxx (reexport)

  • Object.defineProperty(exports|this|module.exports, "xxx", ...)

  • require("abc").xxx

  • require("abc").xxx()

  • 从 ESM 导入

  • require() 一个 ESM 模块

  • 被标记的导出类型 (对非严格 ESM 导入做特殊处理):

    • Object.defineProperty(exports|this|module.exports, "__esModule", { value: true|!0 })
    • exports|this|module.exports.__esModule = true|!0
  • 未来计划支持更多的构造

当检测到不可分析的代码时,webpack 会放弃,并且完全不跟踪这些模块的导出信息(出于性能考虑)。