🌴 Tree shaking
Tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构特性,例如 import 和 export(Webpack5 对 require(CommonJs) 的 Tree shaking 做了支持)。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的,但是这个想法来自于20世纪90年代的LISP。 <webpack文档>
原理
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 提供了一个思路,就是找到作用域之间的关系,来进行优化。
在上面的例子中,其实 function2 和整个 external2 都可以被消去,因为 function2 并没有被 entry 引用到。但是 webpack4 的机制不能做到这一点。
(在生产环境会默认开启 ModuleConcatenationPlugin 插件,将所有的 module 都 inline 去,然后通过 UglifyJS 变相的实现这个功能)
可行性
想想 webpack 的作用: 它从入口遍历所有模块的形成依赖图,并将它们捆绑在一起(bundles)。同时,webpack 知道那些导出被使用。不如我们遍历所有的作用域并将其进行分析,消除未使用的范围和模块的方法。事实上,我们可以把 scope 看作是图中的一个节点。
在上面的代码中,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
给剔除。
上面讲到其实 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 吧!
🧵 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 之前,通常使用:
raw-loader
将文件导入为字符串url-loader
将文件作为 data URI 内联到 bundle 中file-loader
将文件发送到输出目录资源模块类型(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
},
};
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。