前端高級進階:網站的快取控制策略最佳實踐及注意事項

  • 2020 年 3 月 15 日
  • 筆記

對於一個網站來講,性能關乎用戶體驗,你在更短的時間內打開網站,你將會留住更多的用戶。如果你的頁面十秒才能打開,那再好的用戶交互也是徒然。

快取控制是網站性能優化中至為常見及重要的一環,好的快取控制,除了使網站在性能方面有所提升,在財務方面也有重要提升: 更好的快取策略意味著更少的請求,更少的流量,更少的峰值頻寬,從而節省一大筆伺服器或者 CDN 的費用。

快取控制策略就是 http caching 的策略,化繁為簡,最有效的策略往往是很簡單的。在最簡單的粗略下,你對 http cache 只需要了解一個 Cache-Control 的頭部。

一個較好的快取策略只需要兩部分,而它們只需要通過 Cache-Control 控制:

  1. 帶指紋資源: 永久快取
  2. 非帶指紋資源: 每次進行新鮮度校驗

作圖如下:

快取控制策略

帶指紋資源: 永久快取

Cache-Control: max-age=31536000

天下武功,無堅不摧,唯快不破。資源請求最快的方式就是不向伺服器發起請求,通過以上響應頭可以對資源設置永久快取。

  1. 靜態資源帶有 hash 值,即指紋
  2. 對資源設置一年過期時間,即 31536000,一般認為是永久快取
  3. 在永久快取期間瀏覽器不需要向伺服器發送請求

那為什麼帶有 hash 值的資源可以永久快取呢?

因為該文件的內容發生變化時,會生成一個帶有新的 hash 值的 URL。 前端將會發起一個新的 URL 的請求。

非帶指紋資源: 每次進行新鮮度校驗

Cache-Control: no-cache
  1. 由於不帶有指紋,每次都需要校驗資源的新鮮度。(從快取中取到資源,可能是過期資源)
  2. 如果校驗為最新資源,則從瀏覽器的快取中載入資源

index.html 為不帶有指紋資源,如果把它置於快取中,則如何保證伺服器刷新數據時,被瀏覽器可以獲取到新鮮的資源?

因此,使用 Cache-Control: no-cache 時,客戶端每次對伺服器進行新鮮度校驗。

PS:no-cache 與 no-store 的區別是什麼?

即使每次校驗新鮮度,也不需要每次都從伺服器下載資源: 如果瀏覽器/CDN上快取經校驗沒有過期。這被稱為協商快取,此時 http 狀態碼返回 304,指 Not Modified,即沒有變更。

幸運的是,關於協商快取,你無需管理,也無需配置, nginx 或者一些 OSS 都會自動配置協商快取。

而對於協商快取,也有它們自己的演算法,協商快取的背後基於響應頭 Last-Modified/ETag。瀏覽器每次請求資源時,會攜帶上次伺服器響應的 ETag/Last-Modified 作為標誌,與服務端此時的 ETag/Last-Modified 作比較,來判斷內容更改。

http 響應頭中的 ETag 值是如何生成的?

而在作業系統底層,Last-Modified 往往通過文件系統(file system)中的 mtime 屬性生成。而 ETag 提供比 Last-Modified 更精細的檢驗粒度,由文件內容的 hash 或者 mtime/size 生成。當然,這是後話。

一定要為你的資源添加 Cache-Control 響應頭

我會經常接觸到一些網站,他們的資源文件並沒有 Cache-Control 這個響應頭。究其原因,在於快取策略配置這個工作的職責不清,有時候它需要協調前端和運維。

那如果不添加 Cache-Control 這個響應頭會怎麼樣?

是不是每次都會自動去伺服器校驗新鮮度,很可惜,不是。此時會對資源進行強制快取,而對不帶有指紋資訊的資源很有可能獲取到過期資源。 如果過期資源存在於瀏覽器上,還可以通過強制刷新瀏覽器來獲取最新資源。但是如果過期資源存在於 CDN 的邊緣節點上,CDN 的刷新就會複雜很多,而且有可能需要多人協作解決。

那默認的強制快取時間是多少

首先要明確兩個響應頭代表的含義:

  1. Date: 指源伺服器響應報文生成的時間,差不多與發請求的時間等價
  2. Last-Modified: 指靜態資源上次修改的時間,取決於 mtime

LM factor 演算法認為當請求伺服器時,如果沒有設置 Cache-Control,如果距離上次的 Last-Modified 越遠,則生成的強制快取時間越長。

用公式表示如下,其中 factor 介於 0 與 1 之間:

MaxAge = (Date - LastModified) * factor

LM factor

Bundle Splitting:盡量減少資源變更

得益於單頁應用與前端工程化的發展,經過打包後,基本上所有資源都是帶有指紋資訊的,這意味著所有的資源都是能夠設置永久快取。打包策略如下圖所示:

快取控制策略

但僅僅如此了嗎?

如果你所有的 js 資源都打包成一個文件,它確實有永久快取的優勢。但是當有一行文件進行修改時,這一個大包的指紋資訊發生改變,永久快取失效。

所以我們現在需要做到的是:當修改文件後,造成最小範圍的快取失效。webpack 等打包工具雖然在 optimization 上內置了很多性能優化,但它不會幫你做這件事,這件事情需要自己動手。

快取控制策略

此時我們可以對資源進行分層次快取的打包方案,這是一個建議方案:

  1. webpack-runtime: 應用中的 webpack 的版本比較穩定,分離出來,保證長久的永久快取
  2. react/react-dom: react 的版本更新頻次也較低
  3. vendor: 常用的第三方模組打包在一起,如 lodashclassnames 基本上每個頁面都會引用到,但是它們的更新頻率會更高一些。另外對低頻次使用的第三方模組不要打進來
  4. pageA: A 頁面,當 A 頁面的組件發生變更後,它的快取將會失效
  5. pageB: B 頁面
  6. echarts: 不常用且過大的第三方模組單獨打包
  7. mathjax: 不常用且過大的第三方模組單獨打包
  8. jspdf: 不常用且過大的第三方模組單獨打包

隨著 http2 的發展,特別是多路復用,初始頁面的靜態資源不受資源數量的影響。因此為了更好的快取效果以及按需載入,也有很多方案建議把所有的第三方模組進行單模組打包。

小結

快取控制策略