Tree shaking & 初识 Webpack 5

skywalker512

🌴 Tree shaking

Tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构特性,例如 import 和 export(Webpack5 对 require(CommonJs) 的 Tree shaking 做了支持)。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的,但是这个想法来自于20世纪90年代的LISP。 <webpack文档>

1_kdI3LyueVUApUcsmVD24nA.png

原理

ES6 module 特点:

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable 的

ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。

所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们可以动态 require 一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。

这是 ES6 modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS,正是基于这个基础上,才使得 tree-shaking 成为可能,这也是为什么 rollup 和 webpack 都要用 ES6 module syntax 才能 tree-shaking

以前

webpack4 本身的 tree shaking 比较简单,主要是找一个 import 进来的变量是否在这个模块内出现过,非常简单粗暴。但是这种方式往往作用不大,因为一般人不会去 import 一个没有用到的变量。比较多的情况是可能曾经引用过,但是忘了删掉。现在的编辑器和 lint 工具都会提示你去删掉无用的变量,所以 webpack 本身的 tree shaking 功能是不够强大的。

import { isNumber, isNull } from 'lodash-es';

export fun1() {
  // do something
}

export isNull(...args) {
  return isNull(...args);
}

在上面的例子中,变量 isNumber 并没有被引用到,所以会被消去。

开端

2018 年初 webpack 项目下面有一个 issue 提到了 webpack 打包了多余的代码和模块。但是这也为优化 tree-shaking 提供了一个思路,就是找到作用域之间的关系,来进行优化。

1594636505773-33098169-5061-4157-8788-4615709910e3.png

在上面的例子中,其实 function2 和整个 external2 都可以被消去,因为 function2 并没有被 entry 引用到。但是 webpack4 的机制不能做到这一点。

(在生产环境会默认开启 ModuleConcatenationPlugin 插件,将所有的 module 都 inline 去,然后通过 UglifyJS 变相的实现这个功能)
可行性

想想 webpack 的作用: 它从入口遍历所有模块的形成依赖图,并将它们捆绑在一起(bundles)。同时,webpack 知道那些导出被使用。不如我们遍历所有的作用域并将其进行分析,消除未使用的范围和模块的方法。事实上,我们可以把 scope 看作是中的一个节点

1594636477907-36f67758-e48f-4853-8ebe-cad7bc05a970.png

在上面的代码中,deepEqual 与 fun5 相关,equal 与fun6 相关。如果 fun6 不会由另一个模块导入,那么导入的函数 equal 将被消除。

https://diverse.space/webpack-deep-scope-demo/

什么是作用域
// module scope start

    // Block

    { // <- scope start
    } // <- scope end

    // Class

    class Foo { // <- scope start
        //   |       
    }       // <- scope end

  // If else

    if (true) { // <- scope start

    } /* <- scope end */ else { // <- scope start

    } // <- scope end

    // For

    for (;;) { // <- scope start
    } // <- scope end

    // Catch

    try {

    } catch (e) { // <- scope start

    } // <- scope end

    // Function

    function() { // <- scope start
    } // <- scope end

    // Scope

    switch() { // <- scope start
    } // <- scope end

// module scope end

对于 ES6 模块来说,module scope 是最底层的作用域。而对于一个模块来说,只有 class 和 function 的作用域是可以导出到其他模块的。所以在这张需要遍历的图里面,并不是所有的作用域都可以被当作一个独立的遍历结点。

怎样工作

webpack 能提供调用这个模块时候所用到的 scoop1Input: { Used: { scope1 } },继续上面的讲到的分析,这样就能将  isNumber 给剔除。

1594636524723-59c57644-8a92-4388-ad5d-6ab0b0e4aa7a.png

上面讲到其实 issue 提到的方法,有可能与最终的实现有区别,如果有兴趣可以去看相应的 源码 或者 相应合并到 webpack 中的 pr

如何配置

开发环境是默认支持的不需要特别的配置,如果想要在开发环境中看到作用的话可以

// webpack.dev.js
module.exports = {
  ...
  optimization: {
      usedExports: true,
  },
};

然后可以通过这个命令就能看到那些被标记导出了

yarn webpack-cli \--display\-used\-exports \--display\-modules

然后还有一个配置项目在 package.json 中添加 sideEffects 参数,然后这又是一个可以拿来讲的东西了。

🏄‍ sideEffects

先来看一个东西

https://github.com/vuejs/vue/blob/dev/package.json#L15

这是 vue2 的 package.json 他里面讲 sideEffects 设置成了 false,呀 vue 都这样设置我也这样设置算了。

副作用

在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。 例如修改全局变量(函数外的变量),修改参数或改变外部存储。 在某些情况下函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并降低程序的可读性。 严格的函数式语言要求函数必须无副作用。 -> 维基百科)
function go (url) {
  window.location.href = url
}

这个函数修改了全局变量location,甚至还让浏览器发生了跳转,这就是一个有副作用的函数。

现在我们了解了副作用了,那我们怎么才能不让打包工具知道我们的函数没有副作用那?比如这里有两个类,如下所示:

// componetns.js
export class Person {
  constructor ({ name, age, sex }) {
    this.className = 'Person'
    this.name = name
    this.age = age
    this.sex = sex
  }
  getName () {
    return this.name
  }
}

export class Apple {
  constructor ({ model }) {
    this.className = 'Apple'
    this.model = model
  }
  getModel () {
    return this.model
  }
}

// main.js
// 没有用到 Person
import { Apple } from './components.js'

const appleModel = new Apple({
  model: 'IphoneX'
}).getModel()

console.log(appleModel)

如果我们将其使用 babel 打包之后生成的代码是 (模仿实际环境下的情况,具体可以看 你的Tree-Shaking并没什么卵用)

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Person = /*#__PURE__*/function () {
  function Person(_ref) {
    var name = _ref.name,
        age = _ref.age,
        sex = _ref.sex;

    _classCallCheck(this, Person);

    this.className = 'Person';
    this.name = name;
    this.age = age;
    this.sex = sex;
  }

  _createClass(Person, [{
    key: 'getName',
    value: function getName() {
      return this.name;
    }
  }]);

  return Person;
}();
var Apple = /*#__PURE__*/function () {
  function Apple(_ref2) {
    var model = _ref2.model;

    _classCallCheck(this, Apple);

    this.className = 'Apple';
    this.model = model;
  }

  _createClass(Apple, [{
    key: 'getModel',
    value: function getModel() {
      return this.model;
    }
  }]);

  return Apple;
}();

你可能已经注意到了其中有 /*#__PURE__*/ 这个注释,这个就是告诉打包工具我是纯函数没有副作用,你不要我的时候就把我 shaking 掉吧。(详细请看这个 pr 和这个 issues

总结一下:

  • 打包器会将纯函数 tree shaking 掉
  • 可以在 package.json 中将整个项目设定成 sideEffects false 或者单独的文件有副作用
  • 也可以通过  /*#__PURE__*/ 这个注释告诉打包工具是纯函数没有副作用
  • 这太复杂了,因为除非你去翻多个项目的 GitHub 的 issues 和 pr 加上一些实践和翻看别人已经过时文章才能搞懂为啥要这么做,还是用 cra 和 vue-cli 还有 nx.dev

所以到底应不应该设置  sideEffects false ?

在写 npm 库的时候应该,白用白不用,用了还能提高 tree shaking 的效果,在问题出现之前都不是问题,写业务代码都用脚手架,让库作者去决定吧。

所以其实 webpack 里的 sideEffects:false 的意思并不是我这个模块真的没有副作用,而只是为了在摇树时告诉 webpack:我这个包在设计的时候就是期望没有副作用的,即使他打完包后是有副作用的,webpack 同学你摇树时放心的当成无副作用包摇就好啦!。也就是说,只要你的包不是用来做 polyfill 或 shim 之类的事情,就尽管放心的给他加上 sideEffects: false 吧!

https://juejin.im/post/5b4ff9ece51d45190c18bb65

164b0683b455e494.png

🧵 Persistent Caching

这时 Webpack5 新出的一个特性,能将一些东西缓存起来,加快冷启动的速度。

cache: {
  // 1. 选择是将缓存放到文件中
  type: "filesystem",
  
  buildDependencies: {
    // 2. 配置那些文件发生变化需要刷新缓存
      config: [
        __filename,
        path.resolve(__dirname, 'package.json'),
        path.resolve(__dirname, 'webpack.dev.js'),
        path.resolve(__dirname, 'webpack.prod.js')
      ]
  }
}

📦资源模块

资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。

在 webpack 5 之前,通常使用:

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。
// webpack.config.js
const commonConfig = {
  
  module: {
    rules: [
      
      {
        test: /\.png$/,
        type: 'asset/resource'
      }
    ]
  },
  experiments: {
    asset: true
  },
  
};
阅读 2.2k
23 声望
0 粉丝
0 条评论
你知道吗?

23 声望
0 粉丝
宣传栏