FE FAME
首发于FE FAME
对Webpack的hash稳定性的初步探索

对Webpack的hash稳定性的初步探索

背景

众所周知,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-vendorsvendors

更进一步地,对业务代码或许也可以进行这样的拆分,一个系统注定有一部分业务是稳定运行的,一部分是频繁更新和维护的,再一次进行拆分也可以更好地控制缓存有效的部分。

编辑于 2018-03-29

文章被以下专栏收录