本文针对的是`immutable content+long max-age`类型的web缓存。
校验缓存及service worker的处理方案后续有时间再更新。
复制代码
web缓存的好处不用多说,自从webpack一桶江湖后,如何作Predictable long term caching with Webpack让配置工程师们头疼不已。css
webpack4.3前,有至关多的文章介绍如何处理(见参考),这里想作些更到位的探索。html
当业务开发完成,准备上线时,问题就来了🤡:linux
不要放弃治疗🍷本文测试时候的一些版本:webpack
Node.js: v10.8.0
Webpack: v4.17.1
复制代码
contenthash
很爽很安逸🌈HashedModuleIdsPlugin
稳定moduleId。该插件会根据模块的相对路径生成一个四位数的hash做为模块id, 建议用于生产环境🎁HashedModuleIdsPlugin
稳定chunkId。图片、字体等media资源 media资源可使用file-loader
根据资源内容生成hash值,配合url-loader
能够按需内联成base64格式,这里很少说。git
css css资源若是不作特殊处理,会直接打进js文件中;生产环境咱们一般会使用mini-css-extract-plugin
抽取到单独的文件中或是内联。github
js js文件的处理要麻烦的多,做为惟一的入口资源,js管理着其余module,引入了无穷无尽的疑问,这也是咱们接下来的重点。web
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]
。segmentfault
那么他们的区别在哪里呢?windows
简单来讲,当chunk中包含css、wasm时,若是css有改动,chunkhash也会发生改变,致使chunk的哈希值变更;若是使用contenthash,css的改动不会影响chunk的哈希值,由于它是依据chunk 的js内容生成的。缓存
知道有这么几种就够了,下面就从最基本的例子开始吧🚴♂️。
接下来都会在production mode
下测试(若是你不清楚webpack4新增的mode模式,去翻翻webpack mode 文档吧)。
涉及到的拆包策略,会一笔带过,后续有时间再详细聊聊拆包相关的问题~
最简单的配置文件以下👇,
// 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值就会变,来看下面的例子。
让咱们来增长一点点复杂性。
@灰大 在对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]
了。
在webpack4.3以前,咱们只能选择chunkhash进行模块标识,然而这个玩意儿如不是很稳,配置工程师们废了九牛二虎之力用了各类黑科技才将hash值尽量的稳定。
新出的contenthash和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'
能够达到一样的效果,不过须要webapck@4.16.0以上)
打包结果:
// 有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
复制代码
入口文件增长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的哈希值📌 打包后,入口文件的哈希值居然也变了,这就很让人头疼了。
使用contenthash和chunkhash,在上述vendors文件的行为上,有什么样的区别呢? 可否解决因模块变更的问题?
答案是不能😅。 毕竟文件内容中包含了变更的东西,仍是须要HashedModuleIdsPlugin
插件。
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 哈希值在改动先后是彻底一致的💯。
为了优化首屏性能或是业务变得原来越臃肿时,咱们不可避免的会进行一些异步模块的抽取和加载,经过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的哈希值也回归了。
在业务迭代过程当中,常常会增删一些页面,那么这样的场景,哈希值是如何变化的呢?
// webpack.config.js
...
entry: {
index: './src/index.js',
index2: './src/index2.js'
},
...
复制代码
咱们增长一个index2入口文件,内容是一句console.log('i am index2~')
,来看打包结果:
缘由是咱们已经稳定住了ChunkId,各个chunks不会再根据resolving order进行数字自增操做了。
在实际生产环境中,当新引入的chunk依赖了其余公用模块时,仍是会致使一些文件的哈希值变更,不过这个能够经过拆包策略来解决,这里就不赘述了。
本文经过一些例子,总结了经过webpack4作长效缓存的原理以及踩坑实践,并且这些已经运用在了咱们的实际业务中,对于频繁迭代的业务来讲,有至关大的性能提高。
webpack4的长效缓存相比以前的版本有了很大的进步,也有些许不足,可是相信这些在webapck5中都会获得解决🙆♀️~