Skip to content

Tree Shaking原理 #187

Open
Open
@TieMuZhen

Description

@TieMuZhen

一、作用

在webpack对模块进行打包时,将模块中未被使用的冗余代码剔除,仅打包有效代码,精简生成包的体积。

如何编写易于 Tree shaking 的代码

将每个工具函数单独 export,不要集成为一个classtree shaking的最小单元是一个对象,它不能识别一个对象中的哪些函数是需要的

二、如何使用

在 Webpack 中,启动Tree Shaking功能必须同时满足三个条件:

  • 使用 ESM 规范编写模块代码
  • 配置optimization.usedExportstrue,启动标记功能
  • 启动代码优化功能,可以通过如下方式实现:
    • 配置mode = production
    • 配置optimization.minimize = true
    • 提供optimization.minimizer数组

三、实现原理

1. ESModule

我们通过对比ES ModuleCommonJS的区别来理解ES Module的模块机制,它们的区别主要体现在模块的输出和执行上,

  • ES Module输出的是值的引用,而CommonJS输出的是值的拷贝
  • ES Module是编译时执行,而CommonJS模块是在运行时加载

所以ES Module最大的特点就是静态化,在编译时就能确定模块的依赖关系,以及输入和输出的值,这意味着什么?意味着模块的依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,正是基于这个基础,才使得 Tree-Shaking 成为可能,

2. webpack对模块打标记 、删除死代码

借助静态模块分析,Tree-Shaking实现的大体思路:借助 ES6 模块语法的静态结构,通过编译阶段的静态分析,找到没有引入的模块并打上标记,然后在压缩阶段利用像uglify-js这样的压缩工具删除这些没有用到的代码。

涉及知识:

  • 压缩工具的作用:混淆,压缩,最小化,删除不可达代码等;
  • treeShaking依赖于对模块导出和被导入的分析:
    • optimization.providedExports:确定每个模块的导出,默认所有模式都开启
    • optimization.usedExports:确定每个模块下被使用的导出。生产模式下默认开启,其他模式下不开启。
  • webpack对代码进行标记,把importexport标记为3类:
    • 所有import标记为/* harmony import */
    • 被使用过的export标记为/harmony export([type])/
    • 没有被使用的export标记为/* unused harmony export [FuncName] */,其中[FuncName]为export的方法名

四、开发模式和生产模式的默认配置存在差异,其打包方式也存在差异,这里分开讨论:

后面的说明围绕下例展开:

//my-module.js
//my-module.js
export const name = 123;
export const age = 9999;

//index.js
import {name, age} from './test.js';
console.log(name);

development模式

[optimization.usedExports:false]

1)webpack打包(uglifyWebpackPlugin处理前)

全部export被标记为/* harmony export (binding) */

// my-module.js
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "name", function() { return name; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "age", function() { return age; });
var name = 123;
var age = 9999;

2)经过UglifyJSPlugin压缩后不会删除未被使用的导出;

// my-module.js
/* harmony export (binding) */ i.d(e, "name", function() {return t;}), 
/* harmony export (binding) */ i.d(e, "age", function() {return o;});
var t = 123, o = 9999;

结论:usedExports:false时,无法对未使用的接口做处理。

[optimization.usedExports: true]

1)webpack打包(uglifyWebpackPlugin处理前)

未被使用的export会被标记为/* unused harmony export name */,不会使用__webpack_require__.d进行exports绑定;

// my-module.js
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return name; });
/* unused harmony export age */
var name = 123;
var age = 9999;

可以看到age被标记为unused,同时没有使用__webpack_require__.d链接exports
__webpack_require__.d的作用如下:

// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
  if(!__webpack_require__.o(exports, name)) {
    Object.defineProperty(exports, name, { enumerable: true, get: getter });
  }
};

2)经过UglifyJSPlugin压缩后,未使用的接口代码会被删除(如果被别的模块import导入但未被使用,同样会被剔除)。原理显而易见,age未被__webpack_require__.d引用。

// my-module.js
/* harmony export (binding) */ i.d(e, "a", function() {return t;});
/* unused harmony export age */
var t = 123;

不建议在开发环境使用压缩插件。

production模式

1)webpack打包(uglifyWebpackPlugin处理前)

由于生产环境内置了`ModuleConcatenationPlugin`插件,实现"预编译",让webpack根据模块间的关系依赖图中,将所有的模块连接成一个模块,称为"作用域提升"。对于代码缩小体积有很大的提升,也能侧面解决副作用的问题;每个模块会被标记`//CONCATENATED MODULE`。
//被打包到一个作用域内
(function(module, __webpack_exports__, __webpack_require__) {
  //...
  //CONCATENATED MODULE: ./src/my-module.js
  var test_name = 123;
  var age = 9999;
  //CONCATENATED MODULE: ./src/index.js
  console.log(test_name);
}

2)开启uglifyWebpackPlugin:

compress: true;函数的调用会被用函数体替换,使用变量处用其对应值代替,将未使用的变量删除。压缩替换后如下:

function(e, n, o) {
     //...
      // CONCATENATED MODULE: ./src/index.js
      console.log(123);
  }

可以看到导入的age接口未被使用因此被删除,同时优化了多余的中间变量,代码得到精简。

五、sideEffects

在导入时会执行特殊行为的代码,而不是仅仅暴露一个export 或多个 export。比如console.log()polyfillsimport a CSS file等。由于编译器并不知道其是否会影响运行效果,故而不做处理。

在package.json中设置如何处理副作用:

// package.json

//false:表示该模块无副作用代码,若该模块的所有export没有被使用时,可直接删除该模块
//true:表示该模块有副作用代码,该模块要保留副作用代码
"sideEffects": [Boolean], 

或者

// package.json

//[file1,file2]:指定有副作用的文件,在webpack作用域提升时就不会引入
"sideEffects": ['*.css', 'src/tool.js'],

情景1:

import { name } from './module.js';
//name没有使用
import './module.js'

name没有被使用,module.js没有export被使用,且若module.js包含副作用代码

  • sideEffectsfalse,则副作用也被删除。即module整个模块都不会被打包;
  • sideEffectstrue或副作用列表中包含module.js,则会仅保留其副作用代码。

情景2:

import { name } from './module.js'; 
console.log(name)

module.jsname接口被使用,未被使用的其余export都会被删除;无论sidesEffects设置什么值,module.js中的副作用代码始终会被保留。

参考文献

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions