盘点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




