本文針對的是`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資源可使用file-loader
根據資源內容生成hash值,配合url-loader
能夠按需內聯成base64格式,這裏很少說。git
css資源若是不作特殊處理,會直接打進js文件中;生產環境咱們一般會使用mini-css-extract-plugin
抽取到單獨的文件中或是內聯。github
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~')
,來看打包結果:
能夠看到,除了增長了index2.js和runtime~index2.js這兩個文件外,其他文件的哈希值都沒有變更,完美😉
緣由是咱們已經穩定住了ChunkId,各個chunks不會再根據resolving order進行數字自增操做了。
在實際生產環境中,當新引入的chunk依賴了其餘公用模塊時,仍是會致使一些文件的哈希值變更,不過這個能夠經過拆包策略來解決,這裏就不贅述了。
本文經過一些例子,總結了經過webpack4作長效緩存的原理以及踩坑實踐,並且這些已經運用在了咱們的實際業務中,對於頻繁迭代的業務來講,有至關大的性能提高。
webpack4的長效緩存相比以前的版本有了很大的進步,也有些許不足,可是相信這些在webapck5中都會獲得解決🙆♀️~