盘点webpack源码中的那些通用优化手段
前言
大家好这里是阳九,一个文科转码的野路子全栈码农,热衷于研究和手写前端工具.
啃webpack源码也有一阵子了 今天来盘点一些webpack源码中的一些通用优化策略
看看这些库的底层优化到底是怎么做的
lazySet [懒]
lazySet是在webpack中大量运用到的一种数据结构 我们在webpack的主类里就可以看到:
class compilation{
constructor(){
...
/** @type {LazySet<string>} */
this.fileDependencies = new LazySet();
/** @type {LazySet<string>} */
this.contextDependencies = new LazySet();
/** @type {LazySet<string>} */
this.missingDependencies = new LazySet();
/** @type {LazySet<string>} */
this.buildDependencies = new LazySet();
}
}
它是由集合Set封装而来, 与普通的集合不同之处在于,它支持惰性添加元素。 而惰性一词就是优化的关键思想之一, 也就是需要的时候才使用
当我们需要往Set中插入数据时,此时并不会直接将其插入Set,而是给这个元素一个key值,当我们在后面的操作中需要使用这个元素时,才会将其插入Set并返回。
原因: 我们知道,set是有去重功能的。set底层使用哈希表来存储元素,在加入新元素时需要重新计算哈希值,并遍历相应桶进行比较和查找。
如果集合中保存大量数据,那么单次插入并去重会很慢。
deopt
deopt即De-Optimization,是指在Javascript的JIT(Just-In-Time)编译过程中,程序执行时不再满足之前优化假设而导致代码需要重新解析和重建。
比如当一个函数经过多次被调用后,优化编译器会根据该函数已执行的情况来进行特定场景下的加速。
例子
- 当_needMerge属性为true时才会真正去将这些数据merge到set中
- 在执行耗时操作比如keys时会将_deopt设置为false,禁止merge操作
class LazySet {
constructor(iterable) {
this._set = new Set(iterable);
this._toMerge = new Set();
this._needMerge = false;
this._deopt = false;
...
}
// 在其每个方法前面都会检查是否需要merge元素到set中
has(item) {
if (this._needMerge) this._merge();
return this._set.has(item);
}
// 执行keys方法时,会将_deopt关闭,不进行merge操作
keys() {
this._deopt = true;
if (this._needMerge) this._merge();
return this._set.keys();
}
addAll(iterable) {
// 只有在非_deopt的时候才会执行merge操作
if (this._deopt) {
const _set = this._set;
for (const item of iterable) {
_set.add(item);
}
} else {
if (iterable instanceof LazySet) {
if (iterable._isEmpty()) return this;
this._toDeepMerge.push(iterable);
this._needMerge = true;
if (this._toDeepMerge.length > 100000) {
this._flatten();
}
} else {
this._toMerge.add(iterable);
this._needMerge = true;
}
if (this._toMerge.size > 100000) this._merge();
}
return this;
}
}
上述的四个属性: fileDependencies
,contextDependencies
等, 也就是文件依赖项,其元素数量可能非常多,并且在webpack构建的时候会大量进行增查操作。
如果频繁地创建和销毁临时对象,会导致严重的性能问题。而使用lazySet就可以避免这个情况出现。只有当真正需要新增元素时才会去进行相应操作,从而减少了无谓的内存分配和回收次数。
CachedInputFileSystem [缓存]
node中我们一般使用原生fs模块去执行文件操作 而webpack中,将文件系统分为了四份,各自有不同的功能。
而webpack使用自己封装的CachedInputFileSystem作为InputFileSystem
简单的讲,通过这个文件系统的文件内容,都会被直接缓存到内存,使得后续读写加速
compiler.inputFileSystem = new CachedInputFileSystem(fs, 60000);
封装了一些fs模块的常见API, lstat,stat,readdir,readFile,readJson,readlink 以及他们的Sync方法
这个模块一共有两个功能
- 允许Webpack从不同的输入源读取数据(如内存、磁盘等)
- 将通过此系统的文件缓存,下次快速读取
这里以readFile举例,创建了一个Backend(后端缓存), readFile实际执行的是CacheBackend.provide
module.exports = class CachedInputFileSystem {
...
this._readFileBackend = createBackend(
duration,
this.fileSystem.readFile,
this.fileSystem.readFileSync,
this.fileSystem
);
// readFile实际执行CacheBackend.provide
const readFile = this._readFileBackend.provide;
}
由CacheBackend提供的provide方法一共做了两件事情
- 类型检查
- 检查是否缓存过结果,有则直接返回缓存结果
// 创建CacheBackend实例
const createBackend = (duration, provider, syncProvider, providerContext) => {
if (duration > 0) {
return new CacheBackend(duration, provider, syncProvider, providerContext);
}
return new OperationMergerBackend(provider, syncProvider, providerContext);
};
class CacheBackend {
constructor(){
this._data = new Map(); // 保存结果的Map
}
...
provide(path, options, callback) {
// Check in cache
// Run the operation
}
}
memoize [缓存]
memoize是一种函数缓存策略, 术语叫"记忆化函数",webpack中使用的非常简单
通过memoize创建了匿名函数,(闭包),然后将函数结果缓存到闭包内. (如果对闭包概念不清楚的同学建议不往下看了)
在后续每次执行memoized 函数,则会直接获得上一次的结果(cache=true状态)
而不必再重新进行某个高开销的计算。
const memoize = fn => {
let cache = false;
/** @type {T} */
let result = undefined;
return () => {
if (cache) {
return result;
} else {
// 直接将函数结果保存到闭包内
result = fn();
cache = true;
// Allow to clean up memory for fn
// and all dependent resources
fn = undefined;
return result;
}
};
};
这种策略在很多库中都有使用,比如我们常用的国际化组件,format-JS
中,(也就是forrmatMessage),
内部使用了fast-memorize
库,将普通函数转化成记忆化函数,这个库的功能更加强大,算法也经过优化。
import {memoize} from 'fast-memoize';
function expensiveOperation(a, b) {
console.log('Running expensiveOperation!');
return a + b;
}
const memoizedOperation = memoize(expensiveOperation);
console.log(memoizedOperation(1, 2)); // 执行计算并输出 3
console.log(memoizedOperation(1, 2)); // 直接返回缓存的结果,不会执行计算
流处理构建资源 [IO提速]
webpack中有这么几个步骤
- module构建和加载
- chunking 将多个module分割成多个chunk
- optimization 代码优化
- output 输出成文件
而在整个流转的过程中,模块资源都是以流的形式进行计算和传递的。
我们可以看到,在一个构建完毕的module对象中有两个个属性 "_valueAsBuffer" "_valueAsString"
-
在webpack中,模块可以分为很多种,js,css,图片等. 他们无法被统一用字符串表达,
-
像图片、音频等二进制文件不能直接通过js语法表示而必须用字节数组或者Buffer对象进行表示
-
webpack中读写AST 或/及分析抽象句法树(AST)的消耗是很大的, 这些操作对源byte code较友好。
-
Node对IO,流处理的支持更好,整个Webpack构建过程都是基于流处理的
-
对跨平台更友好
总结
一些通用优化策略 比如
- 缓存思想
- 高级数据结构封装
- lazy思想
- 池化思想,并发思想
- 分时思想(分片)
- 流处理 ......
等等 其实都有在底层库里实现, 也是我们平时写码时所需要思考并掌握的技能。
厉害的工程师为了压榨性能, 依然会不断挖掘更多的方案,进行更多算法,数据上的优化
代码优化博大精深
转载自:https://juejin.cn/post/7237425753612648503