Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree Shaking原理 #187

Open
TieMuZhen opened this issue Apr 29, 2022 · 0 comments
Open

Tree Shaking原理 #187

TieMuZhen opened this issue Apr 29, 2022 · 0 comments
Labels

Comments

@TieMuZhen
Copy link
Owner

TieMuZhen commented Apr 29, 2022

一、作用

在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中的副作用代码始终会被保留。

参考文献

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant