Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于webpack4[.3+]构建可预测的持久化缓存方案 #49

Open
jiangjiu opened this issue Sep 8, 2018 · 9 comments
Open

基于webpack4[.3+]构建可预测的持久化缓存方案 #49

jiangjiu opened this issue Sep 8, 2018 · 9 comments

Comments

@jiangjiu
Copy link
Owner

jiangjiu commented Sep 8, 2018

基于webpack4[.3+]构建可预测的持久化缓存方案

本文针对的是`immutable content+long max-age`类型的web缓存。
校验缓存及service worker的处理方案后续有时间再更新。

web缓存的好处不用多说,自从webpack一桶江湖后,如何做Predictable long term caching with Webpack让配置工程师们头疼不已。

webpack4.3前,有相当多的文章介绍如何处理(见参考),这里想做些更到位的探索。

问题

当业务开发完成,准备上线时,问题就来了🤡:

  1. 如何保证不同内容的资源拥有唯一的标识(hash值)?
  2. 修改了业务代码,重新打包,会不会导致所有资源的标识值都变动?
  3. 如果想稳定hash值,如何确保将变动的文件名降到最低?
  4. css/wasm等资源的变动,是否会影响chunk的哈希值?
  5. 业务中引用的顺序改变,是否会改变chunk的哈希值?是否应该?
  6. dynamic import的文件是否支持良好?
  7. 增删多个入口文件,是否会影响已有的哈希值?

不要放弃治疗🍷本文测试时候的一些版本:

Node.js: v10.8.0
Webpack: v4.17.1

TL;DR

  • [email protected]后的contenthash很爽很安逸🌈
  • 使用HashedModuleIdsPlugin稳定moduleId。该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 建议用于生产环境🎁
  • 使用NamedModuleIdsPlugin稳定chunkId。
  • webpack@5会拥有开箱即用的持久化缓存(官方是这样设想的😅,webapck4号称零配置,仍然诞生了大量的高级配置工程师)

需要长效缓存的资源

  • 图片、字体等media资源
    media资源可以使用file-loader根据资源内容生成hash值,配合url-loader可以按需内联成base64格式,这里不多说。

  • css
    css资源如果不做特殊处理,会直接打进js文件中;生产环境我们通常会使用mini-css-extract-plugin抽取到单独的文件中或是内联。

  • js
    js文件的处理要麻烦的多,作为唯一的入口资源,js管理着其他module,引入了无穷无尽的疑问,这也是我们接下来的重点。

webpack4 hash类型

hash类型 描述
hash The hash of the module identifier
chunkhash The hash of the chunk
contenthash (webpack > 4.3.0) The hash of the content(only)

contenthash应该是一个比较重要的feature,webpack核心开发者认为这个可以完全替代chunkhash(见 issue#2096),也许会在webpack5中将contenthash改成[hash]

那么他们的区别在哪里呢?

简单来说,当chunk中包含css、wasm时,如果css有改动,chunkhash也会发生改变,导致chunk的哈希值变动;如果使用contenthash,css的改动不会影响chunk的哈希值,因为它是依据chunk 的js内容生成的。

知道有这么几种就够了,下面就从最基本的例子开始吧🚴‍♂️。

栗子们

接下来都会在production mode下测试(如果你不清楚webpack4新增的mode模式,去翻翻webpack mode 文档吧)。

涉及到的拆包策略,会一笔带过,后续有时间再详细聊聊拆包相关的问题~

1. 简单的hash

最简单的配置文件如下👇,

// webapck.config.js
const path = require('path'); 
const webpack = require('webpack'); 
module.exports = { 
    mode:'production',
    entry: { 
        index: './src/index.js', 
    }, 
    output: { 
        path: path.join(__dirname, 'dist'), 
        filename: '[name].[hash].js',
  }, 
};

入口文件index.js很简单:

// index.js
console.log('hello webapck🐸')

打包结果:

这个例子使用了name + hash进行文件命名,因为hash是根据 module identifier生成的,这意味着只要业务中有一点点小小的改动,hash值就会变,来看下面的例子。

2. 增加一个vendors

让我们来增加一点点复杂性。

@灰大 在对Webpack的hash稳定性的初步探索中展示了一个有趣的例子,我们也来试试看。

现在我们给入口文件增加一个a.js模块:

// index.js
import './a';
console.log('hello webpack🐸');

a模块引入了lodash中的identity方法:

// a.js
import {identity} from 'lodash';
identity();

然后修改下webpack配置文件,以便抽出vendors文件及manifest。这里多说一句,runtimeChunk非常的小,同时可预见的并不会有体积上的大变,所以可以考虑内联进html。

// webapck.config.js
...
module.exports = { 
...
  // 使用splitChunks默认策略拆包,同时提取runtime
   optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all'
        }
    },
};

打包结果是:

[hash] 的问题

相信你已经注意到了,上图打包后,所有的文件都具有相同的hash值,这意味着什么呢?

每一次业务迭代上线,用户端要重新接收静态资源,因为hash值每次都会变动,之前的一切缓存都失效了😬。

所以,我们想要做持久化缓存,肯定是不会用[hash]了。

3. chunkhash了解一下?

在webpack4.3之前,我们只能选择chunkhash进行模块标识,然而这个玩意儿如不是很稳,配置工程师们废了九牛二虎之力用了各种黑科技才将hash值尽可能的稳定。

新出的contenthash和chunkhash有多大的区别呢😳?

来看下面几个例子。

使用chunkhash

我们将[hash]换成[chunkhash],看下打包结果:

index、vendors和runtime都拥有了不同的哈希值,so far so good

我们继续灰大的例子,在index.js中增加b.js模块,b模块只有一行代码:

// index.js
import './b';  // 增加了b.js
import './a';

console.log('hello webpack🐸');
// b.js
console.log('no can no bb');

打包结果:

index文件的哈希值变动符合预期,但是vendors的实质内容仍然是lodash包的identity方法,这个也变了就不能忍了。

原因是webpack4默认按照resolving order使用自增id进行模块标识,所以插入了b.js导致vendors的id错后了一个数,这一点我们diff一下两个vendors文件就可以看出,两个文件只有这里不同:

灰大文章中也提到了,解决方案很简单,使用HashedModuleIdsPlugin,这是一个内置插件,它会根据模块路径生成模块id,问题就迎刃而解了:

(起初比较担心根据module path进行hash计算后命名,这样的方式是否会因操作系统不同而产生差异,毕竟已经吃过一次亏了,见windows/linux下path路径不一致的问题 ,好在webpack官方已经处理过这个问题了,无需操心了)

// webpack.config.js
...
plugins:[
    new webpack.HashedModuleIdsPlugin({
        // 替换掉base64,减少一丢丢时间
        hashDigest: 'hex'
    }),
]
...

(设置optimization.moduleIds:'hash'可以达到同样的效果,不过需要[email protected]以上

打包结果:

// 有b模块时:
        index.a169ecea96a59afbb472.js  243 bytes       0  [emitted]  index
vendors~index.6b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.ec8eb4cb2ebdc83c76ed.js   1.42 KiB       2  [emitted]  runtime~index

// 没有b模块时:
        index.8296fb0301ada4a021b1.js  185 bytes       0  [emitted]  index
vendors~index.6b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.ec8eb4cb2ebdc83c76ed.js   1.42 KiB       2  [emitted]  runtime~index

4. 增加一个css 模块

入口文件增加c.css👇,c的内容不重要:

// index.js
import './c.css';
import './b';
import './a';
...

配置一下mini-css-extract-plugin将这个css模块抽取出来:

// webpack.config.js
...
module: {
        rules: [
            {
                test: /\.css$/,
                include: [
                    path.resolve(__dirname, 'src')
                ],
                use: [
                    {loader: MiniCssExtractPlugin.loader},
                    {loader: 'css-loader'}
                ]
            }
        ]
    },
plugins:[
    new webpack.HashedModuleIdsPlugin(),
    // 增加css抽取
    new MiniCssExtractPlugin({
        filename: '[name].[contenthash].css',
        chunkFilename: '[name].[contenthash].css'
    })
]
...

然后打包。
改动一点c.css中的内容,再次打包。

这两次打包过程,我们只对c.css文件做了改动,预期是什么呢?
当然是希望只有css文件的哈希值有改动,然而事情并不符合预期:

// 增加了c.css
                                Asset       Size  Chunks             Chunk Names
       index.90d7b62bebabc8f078cd.css   59 bytes       0  [emitted]  index
        index.e5d6f6e2219665941029.js  276 bytes       0  [emitted]  index
vendors~index.6b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.de3e5c92fb3035ae4940.js   1.42 KiB       2  [emitted]  runtime~index

// 改动c.css中的代码后
                                Asset       Size  Chunks             Chunk Names
       index.22b9c488a93511dc43ba.css   94 bytes       0  [emitted]  index
        index.704b09118c28427d4e8f.js  276 bytes       0  [emitted]  index
vendors~index.6b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.de3e5c92fb3035ae4940.js   1.42 KiB       2  [emitted]  runtime~index

注意看index.js的哈希值📌
打包后,入口文件的哈希值竟然也变了,这就很让人头疼了。

5. contenthash治愈一切?

contenthash并不能解决moduleId自增的问题

使用contenthash和chunkhash,在上述vendors文件的行为上,有什么样的区别呢?
能否解决因模块变动的问题?

答案是不能😅。
毕竟文件内容中包含了变动的东西,还是需要HashedModuleIdsPlugin插件。

contenthash威力所在

contenthash可以解决的是,css模块修改后,js哈希值变动的问题。

修改配置文件👇:

...
    output: {
        path: path.resolve(__dirname, './dist'),
        // 改成contenthash
        filename: '[name].[contenthash].js'        
    },
...    

直接来看对比:

// 增加了c.css
                                Asset       Size  Chunks             Chunk Names
       index.22b9c488a93511dc43ba.css   94 bytes       0  [emitted]  index
        index.41e5e160a222e08ed18d.js  276 bytes       0  [emitted]  index
vendors~index.ec19a3033220507df6ac.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.d25723c2af2e039a9728.js   1.42 KiB       2  [emitted]  runtime~index

// 改动c.css中的代码后
                                Asset       Size  Chunks             Chunk Names
       index.a4afb491e06f1bb91750.css   60 bytes       0  [emitted]  index
        index.41e5e160a222e08ed18d.js  276 bytes       0  [emitted]  index
vendors~index.ec19a3033220507df6ac.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.d25723c2af2e039a9728.js   1.42 KiB       2  [emitted]  runtime~index

可以看到,index.js的chunk 哈希值在改动前后是完全一致的💯。

6. 增加异步模块

为了优化首屏性能或是业务变得原来越臃肿时,我们不可避免的会进行一些异步模块的抽取和加载,通过dynamic import方式就很安逸。

然而,异步模块作为一个新的chunk,他的哈希值是啥样的嘞?

我们增加一个异步模块试试看。

// webpack.config.js
...
output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].[contenthash].js',
        // 增加chunkFilename
        chunkFilename: '[name].[contenthash].js'
},
...    
// async-module.js
export default {
    content: 'async-module'
};


// index.js
import './c.css';
import './b';
import './a';
// 增加这个模块
import('./async-module').then(a => console.log(a));

console.log('hello webpack🐸');

async-module的内容也是不重要,重要的是增加这个模块前后的哈希值有了很大的变化!
没有异步模块:

增加异步模块:

再增加第二个异步模块:

上面的对比简直是一夜回到解放前。。。除了css文件的哈希值在线,其他的都发生了改变。

究其原因,是因为虽然我们稳定住了moduleId,但是对chunkId无能为力,而且异步的模块因为没有chunk.name,导致又使用了数字自增进行命名。

好在我们还有NamedChunksPlugin可以进行chunkId的稳定👇:

// webapck.config.js
...
plugin:{
      new webpack.NamedChunksPlugin(
            chunk => chunk.name || Array.from(chunk.modulesIterable, m => m.id).join("_")
     ),
        ...
}
...

除此之外还有其他的方式可以稳定chunkId,不过由于或多或少的缺点在这里就不赘述了,来看现在打包的结果:

可以看出,异步模块也都有了name值,同时vendors的哈希值也回归了。

7. 增加第二个入口文件

在业务迭代过程中,经常会增删一些页面,那么这样的场景,哈希值是如何变化的呢?

// webpack.config.js
...
entry: {
        index: './src/index.js',
        index2: './src/index2.js'
    },
...    

我们增加一个index2入口文件,内容是一句console.log('i am index2~'),来看打包结果:


可以看到,除了增加了index2.js和runtime~index2.js这两个文件外,其余文件的哈希值都没有变动,完美😉

原因是我们已经稳定住了ChunkId,各个chunks不会再根据resolving order进行数字自增操作了。

在实际生产环境中,当新引入的chunk依赖了其他公用模块时,还是会导致一些文件的哈希值变动,不过这个可以通过拆包策略来解决,这里就不赘述了。

总结

本文通过一些例子,总结了通过webpack4做长效缓存的原理以及踩坑实践,而且这些已经运用在了我们的实际业务中,对于频繁迭代的业务来说,有相当大的性能提升。

webpack4的长效缓存相比之前的版本有了很大的进步,也有些许不足,但是相信这些在webapck5中都会得到解决🙆‍♀️~

参考

@beyondghx
Copy link

文章写的很赞。TL;DR 部分第三点是不是写错了,应该是用 NamedChunksPlugin 插件来稳定 chunk的 id吧?另外真实场景是 NamedChunksPluginHashedModuleIdsPlugin 是一起用的吗?

@jiangjiu
Copy link
Owner Author

文章写的很赞。TL;DR 部分第三点是不是写错了,应该是用 NamedChunksPlugin 插件来稳定 chunk的 id吧?另外真实场景是 NamedChunksPluginHashedModuleIdsPlugin 是一起用的吗?

感谢指正。
在我们的业务线上,这两个是一起使用的。

@wd2010
Copy link

wd2010 commented Dec 4, 2018

楼主,你的demo还在吗,我在异步加载时配置了NamedChunksPlugin还是全部生成了一遍,想看下你的demo

@hdmy
Copy link

hdmy commented Apr 11, 2019

(设置optimization.moduleIds:'hash'可以达到同样的效果,不过需要[email protected]以上)

这里应该为 moduleIds: 'hashed',最靠谱的官方文档

@dadaa1
Copy link

dadaa1 commented Jul 12, 2019

楼主,看了您的文章受益匪浅,非常感谢。
但是我对你的
6. 增加异步模块
这里有点疑问,使用HashedModuleIdsPlugin已经把chunk id给稳定住了,就不再需要NamedChunksPlugin了吧,我尝试下,发现没有NamedChunksPlugin效果是一样的(Version: webpack 4.35.2 )。
我查看(文档)[https://webpack.docschina.org/guides/caching/#%E6%A8%A1%E5%9D%97%E6%A0%87%E8%AF%86%E7%AC%A6-module-identifier-],也是说这两个插件的功能是相同的。

@jiangjiu
Copy link
Owner Author

楼主,看了您的文章受益匪浅,非常感谢。
但是我对你的
6. 增加异步模块
这里有点疑问,使用HashedModuleIdsPlugin已经把chunk id给稳定住了,就不再需要NamedChunksPlugin了吧,我尝试下,发现没有NamedChunksPlugin效果是一样的(Version: webpack 4.35.2 )。
我查看(文档)[https://webpack.docschina.org/guides/caching/#%E6%A8%A1%E5%9D%97%E6%A0%87%E8%AF%86%E7%AC%A6-module-identifier-],也是说这两个插件的功能是相同的。

在webpack4.3时,异步模块是没有chunk Name的,所以会使用数字自增。
我看文档里只是说明了同步模块,你试试异步模块会不会有问题?

@dadaa1
Copy link

dadaa1 commented Jul 15, 2019

楼主,看了您的文章受益匪浅,非常感谢。
但是我对你的
6. 增加异步模块
这里有点疑问,使用HashedModuleIdsPlugin已经把chunk id给稳定住了,就不再需要NamedChunksPlugin了吧,我尝试下,发现没有NamedChunksPlugin效果是一样的(Version: webpack 4.35.2 )。
我查看(文档)[https://webpack.docschina.org/guides/caching/#%E6%A8%A1%E5%9D%97%E6%A0%87%E8%AF%86%E7%AC%A6-module-identifier-],也是说这两个插件的功能是相同的。

在webpack4.3时,异步模块是没有chunk Name的,所以会使用数字自增。
我看文档里只是说明了同步模块,你试试异步模块会不会有问题?

您好,我尝试下异步模块,确实会有这个问题,必须需要加入NamedChunksPlugin去解决,但是我现在的困惑更多了,NamedChunksPlugin和HashedModuleIdsPlugin有什么区别,为什么文档上写着只采用一个即可,这里说的不同mode下开启不同plugin,这些plugin没找到文档呢?

@jiangjiu
Copy link
Owner Author

楼主,看了您的文章受益匪浅,非常感谢。
但是我对你的
6. 增加异步模块
这里有点疑问,使用HashedModuleIdsPlugin已经把chunk id给稳定住了,就不再需要NamedChunksPlugin了吧,我尝试下,发现没有NamedChunksPlugin效果是一样的(Version: webpack 4.35.2 )。
我查看(文档)[https://webpack.docschina.org/guides/caching/#%E6%A8%A1%E5%9D%97%E6%A0%87%E8%AF%86%E7%AC%A6-module-identifier-],也是说这两个插件的功能是相同的。

在webpack4.3时,异步模块是没有chunk Name的,所以会使用数字自增。
我看文档里只是说明了同步模块,你试试异步模块会不会有问题?

您好,我尝试下异步模块,确实会有这个问题,必须需要加入NamedChunksPlugin去解决,但是我现在的困惑更多了,NamedChunksPlugin和HashedModuleIdsPlugin有什么区别,为什么文档上写着只采用一个即可,这里说的不同mode下开启不同plugin,这些plugin没找到文档呢?

webpack文档一直不算太好,而且内部机制过于复杂,个别稍微小众的plugin就需要去专门的github看issue和源码了。
多写demo测试,多翻翻源码,大概就可以整理一个思路出来了。

@dadaa1
Copy link

dadaa1 commented Jul 18, 2019

楼主,看了您的文章受益匪浅,非常感谢。
但是我对你的
6. 增加异步模块
这里有点疑问,使用HashedModuleIdsPlugin已经把chunk id给稳定住了,就不再需要NamedChunksPlugin了吧,我尝试下,发现没有NamedChunksPlugin效果是一样的(Version: webpack 4.35.2 )。
我查看(文档)[https://webpack.docschina.org/guides/caching/#%E6%A8%A1%E5%9D%97%E6%A0%87%E8%AF%86%E7%AC%A6-module-identifier-],也是说这两个插件的功能是相同的。

在webpack4.3时,异步模块是没有chunk Name的,所以会使用数字自增。
我看文档里只是说明了同步模块,你试试异步模块会不会有问题?

您好,我尝试下异步模块,确实会有这个问题,必须需要加入NamedChunksPlugin去解决,但是我现在的困惑更多了,NamedChunksPlugin和HashedModuleIdsPlugin有什么区别,为什么文档上写着只采用一个即可,这里说的不同mode下开启不同plugin,这些plugin没找到文档呢?

webpack文档一直不算太好,而且内部机制过于复杂,个别稍微小众的plugin就需要去专门的github看issue和源码了。
多写demo测试,多翻翻源码,大概就可以整理一个思路出来了。

好的,谢谢~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants