背景
众所周知,IO是一个很蛋疼的东西,为此我们大量引入异步的操作,甚至改变了编程的模式。而网络更是蛋疼中的蛋疼,其延迟、速度和不可预测性都让Web应用万分难堪。
现代的Web应用的性能其实很大程度上是建立在缓存的基础上的,而其中最为常见的一种最大化利用缓存的形式就是为静态资源加上hash,使用一个不会重复的标识符来达到资源可以永久缓存的目的。
hash的重要性在于它决定了缓存的失效与否,进一步决定了缓存的利用率。在一个迭代更频繁的系统中,hash的稳定性——即尽量保证一个资源的hash在多次发布过程中保持不变——对性能的影响就更为显著。
默认的hash稳定性
第一步,我们希望看一下webpack自身对hash的稳定性是怎么处理的,因此造了一个非常简单的结构:
/src
index.js
a.js
b.js
webpack.config.js在a.js中,我们简单地引用并使用一下著名的lodash库:
import {identity} from 'lodash';
identity();然后作为入口的index.js去引用a.jx:
import './a';为了让webpack能够拆出相应的chunk来,对webpack作了一些简单的配置:
const path = require('path');
module.exports = {
mode: 'production',
context: path.join(__dirname, 'src'),
entry: {
index: './index.js',
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].[chunkhash].bundle.js',
chunkFilename: '[name].[chunkhash].bundle.js',
publicPath: '/',
},
optimization: {
splitChunks: {
chunks: 'all',
},
runtimeChunk: true,
}
};随后运行webpack(4.3.0),看一下构建后的结果:
runtime~index.bc3506a5bf6b5c36ca7d.bundle.js 1.04 KiB 0 [emitted] runtime~index
vendors~index.d0b760b44e3276af20c4.bundle.js 69 KiB 1 [emitted] vendors~index
index.ffce817c9dbe9b136009.bundle.js 140 bytes 2 [emitted] index随后,我们让index.js去同时引用b.js(该文件是空的,它的内容并不重要):
import './a';
import './b';再次运行webpack,我们得到这样的结果:
runtime~index.bc3506a5bf6b5c36ca7d.bundle.js 1.04 KiB 0 [emitted] runtime~index
vendors~index.3f86a94c0120d9b7fb8b.bundle.js 69 KiB 1 [emitted] vendors~index
index.f770b0f6781e083cd18b.bundle.js 161 bytes 2 [emitted] index可以看到,除了预期内的index的hash产生了变化外,vendors也同样改变了其hash,哪怕我们从头到尾都只引用了一个lodash,理论上vendors中的内容没有变化(事实上写这篇的原因就是我天真地以为webpack 4所谓0配置已经解决了这一问题)。随后我们用MD5验证了其内容确实不同:
MD5 (dist/vendors~index.3f86a94c0120d9b7fb8b.bundle.js) = ab1f3168f7f7b96b9be60b8f701c05c7
MD5 (dist/vendors~index.d0b760b44e3276af20c4.bundle.js) = 4f6d1aa1f5aa21b55b20791b20c11ec4稳定webpack的hash
从上面的实验我们得到一个结论:在默认情况下,哪怕一个chunk中的实际内容没有变化,其hash也会因其它chunk的变化变得不同。
这并不是一个好现象,我们辛辛苦苦拆分好chunk,将第三方的内容单独打包,结果每次仅仅修改业务代码就导致整个第三方包的hash不断变化,缓存一次次失效,无法享受性能的优势。
造成这一hash不稳定现象的根本原因在于webpack使用自增的数字(好像是这样)作为每一个模块的id,因此在上面的案例中,由于插入了b这个模块占用了一个id,导致lodash对应的id发生了变化,最终引起了hash的变化。
当然这个问题是能够得到很好的解决的,webpack自带了一个HashedModuleIdsPlugin插件,其作用就是使用另一种算法(模块路径做个hash)来生成模块的id,我们只需要简单地加上这个插件:
const {HashedModuleIdsPlugin} = require('webpack');
module.exports = {
...
plugins: [
new HashedModuleIdsPlugin()
]
};然后在没有对b.js的引用,以及有这一引用的情况下,构建出2个结果:
# 没有b.js引用
runtime~index.bc3506a5bf6b5c36ca7d.bundle.js 1.04 KiB 0 [emitted] runtime~index
vendors~index.56acee66d1b795ab6271.bundle.js 69.1 KiB 1 [emitted] vendors~index
index.f9cd41f3b57586b9414a.bundle.js 154 bytes 2 [emitted] index
# 有b.js引用
runtime~index.bc3506a5bf6b5c36ca7d.bundle.js 1.04 KiB 0 [emitted] runtime~index
vendors~index.56acee66d1b795ab6271.bundle.js 69.1 KiB 1 [emitted] vendors~index
index.78278506ec7601b6aed9.bundle.js 185 bytes 2 [emitted] index可以看到,虽然index的hash变化了(因为多了b模块,符合预期),vendors的hash还是保持不变的。这就有助于我们在每一个迭代发布后,只要不增加或减少对第三方库的引用,就可以让vendors无限期地使用缓存。
教训与经验
首先,这一研究让我们深刻地意识到一个问题:
别听webpack瞎说0配置,也别听他瞎说自己的chunk很智能,真跑一跑到处是问题,webpack工程师这职位少不了。
而后,我们又意识到另一个关键点:
一次构建的上下文不仅仅是当前的代码,更需要关注上一次(甚至是下一次)构建,将这些都作为上下文才能达到最好的优化。
进一步的思考
虽然已经解决了在不增加或减少第三方库的前提下的vendors的hash稳定性,我们依旧面临着一个问题:在npm社区这种大量单一功能小体积的包的哲学指导下,第三方库的增减几乎成为必然。
为了解决这一问题,我们将继续进行探索,根据实际第三方库使用的稳定性(即是不是必须用,是不是会频繁的在用和不用间切换)来进一步地拆分stable-vendors和vendors。
更进一步地,对业务代码或许也可以进行这样的拆分,一个系统注定有一部分业务是稳定运行的,一部分是频繁更新和维护的,再一次进行拆分也可以更好地控制缓存有效的部分。