合理利用快取是Web效能優化的必要手段。而增量更新是目前大部分團隊採用的快取更新方案,結合HTTP強制快取策略,既能夠保證使用者在第一時間獲取最新資源,又可以減少網路資源消耗,提高Web應用程式的執行速度。
覆蓋更新和增量更新
覆蓋更新3年前用得比較多,現在已經逐漸淘汰,我們先引用一個簡單場景來說明一下兩者的區別和增量更新的優勢。
假設專案中存在一個css檔案和一個js檔案,由 index.html 引入:
<html>
<head>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<script src="index.js" />
</body>
</html>
複製程式碼
為了提高頁面載入效能,我們啟動瀏覽器強制快取策略,index.css 和 index.js 均被快取到本地,如果在快取有效期內發生了迭代,為了保證讓使用者第一時間獲取最新內容,就必須讓瀏覽器放棄之前的快取檔案而繼續請求伺服器並下載最新的資源。
覆蓋更新方案
覆蓋更新的方案和他的名字一樣粗暴,在引用資源的URL後新增請求引數,比如新增一個版本號資訊:
<html>
<head>
<link rel="stylesheet" href="index.css?v=1.0.0" />
</head>
<body>
<script src="index.js?v=1.0.0" />
</body>
</html>
複製程式碼
這樣 inde.css 和 index.js 會被重新請求並下載,確實起到了更新快取的作用。如果下一波迭代只改動了 index.js ,按照這個方案可以只增加 index.js 的版本號:
<html>
<head>
<link rel="stylesheet" href="index.css?v=1.0.0" />
</head>
<body>
<script src="index.js?v=1.0.1" />
</body>
</html>
複製程式碼
有針對性的引數修改工作對開發人員來說並不困難,因為參與開發的人員知道哪些檔案改動了,但專案一旦大了或迭代次數多了就會越來越繁瑣。工程化的思想指導我們使用工具替代人工,但工具沒有記憶,想要讓工具識別改動的檔案並針對性地修改版本號引數有兩個方案:
- 通過人工記錄改動的檔案列表並讓工具讀取此配置(還是沒有解放人力❌)
- 讓工具自動獲取檔案改動之前的內容後逐一對比(比較耗時❌)
雜湊指紋
解決上面的差異更新問題,我們可以先從版本號上做文章。其實URL後面的v引數目的就是讓瀏覽器重新請求該資源,那能否讓這個引數和檔案內容自動地一一對應呢?這就是雜湊指紋,我們可以用md5演算法計算出檔案雜湊值,然後用雜湊指紋替換最初的版本號:
<html>
<head>
<link rel="stylesheet" href="index.css?v=858d5483" />
</head>
<body>
<script src="index.js?v=bbcaaf73" />
</body>
</html>
複製程式碼
看起來挺不錯,以前確實見到過一些網站應用過這種方案。但仔細想想還是存在2個問題:
更新部署同步性問題
必須確保 html 檔案與改動的靜態資原始檔同步更新,否則會出現資源不同步的情況。目前大多數團隊的部署方式是將網站的入口HTML和靜態資源JS/CSS/圖片等分開部署(即常見的靜態資源託管)。兩種資源分開部署必然會有先後順序,也就是資源上線的時間差,這個時間差可大可小,具體影響取決於這個網站的流量規模。這也是很多釋出選在凌晨或半夜這種網站訪問量較低的時間段進行的原因之一。
不利於版本回滾
由於覆蓋更新每次都是迭代之後的資源覆蓋伺服器上原有的舊版本(也就是伺服器上永遠只存一份最新的內容),這對版本回滾就不友好了。雖然運用git的版本回退,然後快速覆蓋部署能緩解一定的壓力,但這遠沒有直接在伺服器上使用老版本的構建產品來得快。
增量更新方案
增量更新策略完美地解決了上述缺陷,實現的方案很簡單,將原本作為引數值的雜湊指紋,作為資原始檔名的一部分,並且刪除用於更新的url引數:
<html>
<head>
<link rel="stylesheet" href="index.858d5483.css" />
</head>
<body>
<script src="index.bbcaaf73.js" />
</body>
</html>
複製程式碼
在靜態資源使用增量更新策略的前提下,可以將靜態資源先於動態HTML部署,此時靜態資源沒有引用入口,不會對線上緩解產生影響;動態HTML部署後即可在第一時間訪問已存在的最新靜態資源。這樣並很好地解決了更新部署同步性的問題。另一方面,增量更新不會覆蓋舊版本檔案,運維回滾時只需回滾HTML即可,這樣不僅優化了版本控制,而且還可以支援多版本共存的需求,perfect!
按需載入與多模組構建場景
多模組構建就是指存在多個互不干擾的模組體系,這些模組體系可能存在同一頁面,也可能存在於兩個獨立的頁面,需要按需載入。這類場景下需要考慮以下2個問題:
- 第一,同步模組的修改對非同步檔案和主檔案雜湊指紋產生的影響
- 第二,非同步模組的修改對主檔案雜湊指紋產生的影響
先來說說第一點
假設一個單頁面應用的模組結構如下:
- 主模組
main.app.js
- 同步模組
module.sync.js
(構建後與主模組合併為main.app.[hash].js
,同步載入) - 非同步模組
module.async.js
(單獨構建為非同步檔案app.async.[hash].js
,按需載入)
從上:構建輸出的檔案雜湊值會在參與計算的模組內容改動後產生變化,同步模組 module.sync.js 的內容作為計算因子參與主檔案的雜湊指紋,並未參與非同步檔案hash指紋的計算。所以可以確定的是,同步模組的修改影響主模組的雜湊指紋,對非同步檔案無影響。
非同步模組的修改對主模組的影響
非同步模組的內容隻影響非同步檔案的雜湊指紋,是這樣的嗎?在此之前我們先搞清楚非同步檔案的載入原理:
window.onload = function() {
var script = document.createElement('script');
script.src = 'https://static.app.com/async.js';
document.head.append(script);
}
複製程式碼
非同步檔案的URL被固定寫死在負責載入它的主檔案中,如果應用了雜湊指紋:
window.onload = function() {
var script = document.createElement('script');
script.src = 'https://static.app.com/async.2483feal.js';
document.head.append(script);
}
複製程式碼
如果我們假設非同步模組更新了只修改了非同步模組的雜湊指紋(從 async.2483feal.js
變成 async.3234afcb.js
),而主模組的雜湊指紋保持不變,即主模組里載入的非同步模組 依然是 async.2483feal.js
。
顯然,這並不是我們想要的結果,所以雖然有點不情願,非同步模組修改不僅僅影響自己的雜湊指紋,主模組的雜湊指紋也要跟著改變才能起到更新的作用。
如何用webpack實現增量更新
用Webpack實現增量更新,顧名思義就是給構建產物(JS和CSS檔案)新增雜湊指紋
配置JS檔案輸出
核心就是Webpack編譯生成js檔案追加hashcode:
{
entry: './app.js',
output: {
filename: 'js/[name].[chunkhash:7].js',
chunkFilename: 'js/[name].[chunkhash:7].js',
}
}
複製程式碼
配置CSS檔案輸出
給CSS檔案也新增雜湊指紋可以通過外掛 mini-css-extract-plugin
來實現:
onst MiniCssExtractPlugin = require('mini-css-extract-plugin')
{
...
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:7].css'
})
]
}
複製程式碼
值得注意的是JS雜湊指紋使用 chunkhash
來生成,而CSS則使用 contenthash
,兩者的區別在於:
chunkhash
是指Webpack在打包chunk
塊時,根據chunk
塊內容生成的雜湊指紋,檔案內容不改變則其雜湊值不變。而 contenthash
並不是Webpack自身的另外一種雜湊值,而是代表被匯出內容計算後的雜湊值,其值是相對主檔案(JS)完全獨立的。
因為CSS是通過JS模組匯入的,所以理論上CSS也屬於JS的內容部分,CSS內容改變時JS的雜湊指紋也會跟著變化,這顯然不是我們想要的結果,而 contenthash
就是解耦JS與CSS檔案雜湊指紋的關鍵,於是我們可以通過contenthash
讓JS檔案改變時CSS檔案雜湊指紋保持不變。