新手坑:讓人又愛又恨的 HTTP Caching

當顯示一個網頁需要的資源(.html, .js, .css, 圖片)越來越多、檔案尺寸越來越肥大時,使用 HTTP Cache 幾乎是每個人都會採取的必要手段,一來節省資料流量,二來視覺上頁面渲染完成的時間縮短很多,有種寫的 code 效能變好的感覺 (誤)。

先說說好處。
試想,如果每次打開 Facebook,每張之前看過的圖片都不使用瀏覽器快取,要重新下載一次,每次吃掉的資料量會有多少?若我不是用 WiFi 或手機流量吃到飽,大概就放棄打開 Facebook 了。

而若沒有中介快取(例如:CDN)的存在,每次都要跟很遠的 server 要資料,估計下載資料要蠻久的,等久了也不耐煩,不想再使用該服務。

以上提的是,快取帶來的好處,接著來說說壞處,尤其常發生在開發者身上。

問:為什麼我已經 deploy 了一版新的 code,browser 也已經 reload 好多次,還是看不到我改的功能?
問:設計師請我把伺服器上的一張圖片資源更新,我替換掉了,但為什麼我瀏覽器還是一直出現舊的圖?
答:一切都是快取搞的啊~

上述兩個問題似乎是新手容易踩的坑,以下來談:

  1. 從發出請求到實體靜態資源之間發生什麼事?
  2. 從 HTTP response 看資源的 cache policy
  3. 替檔名加上指紋碼 ( hash )
  4. 檔名不同和Request 參數不同皆視為請求不同資源

從發出請求到實體靜態資源之間發生什麼事?

讓我們將情形簡單化,不存在中介快取,假設之前請求過一個檔案 GET /app.js,其 response header 有定義 Cache-Control: public, max-age = 86400ETag: 'x45677',簡單說明

  1. 這是一個可以被快取的檔案 app.js
  2. 若本地端有可使用的快取,不用詢問 server 就可直接使用 (關於更多設定請看下面的 Cache-control directives )。
  3. 快取自第一次請求後可重複使用 86400秒 = 1 天。
    瀏覽器發出 request 要求 GET /app.js 時 的流程如下:
用戶端瀏覽器發出檔案請求的流程

若有瀏覽器快取有檔案且 not stale (依然新鮮),可以直接用

從 HTTP response 看 cache policy

資源的 cache policy 是由 server side 控制,但我認為前端工程師和設計師藉由了解這個機制和學會怎麼從瀏覽器的 developer tool 中檢查快取使用情形,都可以讓整個開發與確認過程更順利。

打開瀏覽器中的「開發者工具」( Google Developer — 如何開啟 Chrome 開發者工具),選擇 上方「Network」分頁,重載 Medium 頁面,可以看到一些資源檔的 response 回來,點選其中一個來看:

medium.com的資源請求response

Cache-Control directives

HTTP request response中的cache-control 屬性根據 RFC2616 文件可以有以下 directives,但只會挑常用的幾個出來講 :

// 決定可以儲存response的快取是哪種
// 是私有的 private cache (eg.瀏覽器快取 )
// 還是公用的 shared cache (如: CDN )
public : response可被任何共用、私有快取儲存
private : respone只能被私有快取儲存,共用快取不行,通常用於個人敏感資料
// 可不可以存快取
no-store : request和response的內容都完全不存,每次都要從server拿資源
no-cache : 可以存快取,但使用快取前每次都要送request 問server,server沒有更新的檔案版本才可以用
// 新鮮度
max-age : 指定從目前請求開始,允許擷取的回應重複使用的最長時間 (單位為秒)

因此, Cache-control: private, max-age=600 表示用戶端瀏覽器最長只能快取回應使用 10 分鐘 (60 秒 x 10 分),之後就要向伺服器詢問是否有新版本。

認證權杖 ETag

ETag 是一組由 server 計算的 hash ,就像是大家常聽到的 md5 一樣,server用一套演算法函數,根據實體檔案的一些資訊去計算出一個 hash code 來當檔案認證碼,不同的檔案丟進這個函數就會得到不同的hash code。

用戶端在 max-age 過期後,向伺服器發送檔案 request 時會夾帶這 ETag 資訊,伺服器會計算伺服器上的該檔案 ETag 是否已改變,若是,則傳送該檔案,若沒有改變,則會回傳 status code : 304 (Not Modified) 告訴用戶端,沒有新版本,使用快取即可。

替檔名加上指紋碼 ( hash )

從上方的開發者工具畫面可以看到,該檔名的格式為 main-base.bundle.<hash>.js ,這是前端工程師在 build code 時可以介入的部分,替檔名加上hash。

前端部分我都使用 Webpack build code,使用了 HtmlWebpackPlugin 並打開 hash 功能

const HtmlWebpackPlugin = require('html-webpack-plugin');
const plugins = [ 
new HtmlWebpackPlugin({
template: './src/ejs/index.ejs',
title: 'Example',
hash: true, // 加上這個屬性
}),
];

檔名不同和 Request 參數不同皆視為請求不同資源

舉例來說,假設瀏覽器中沒有任何快取資料,依序送出以下三個資源請求:

  1. GET /app.bundle.js
  2. GET /app.bundle.12345.js
  3. GET /app.bundle.js?v=12345

第二及第三種請求都會被認定是與第一種不同的 resource request,請求 server 返回資源檔案。


小結

快取的使用通過以下三關守門員的檢查 ,沒過第一關就不會到第二關,依此類推:

  1. 瀏覽器檢查 HTTP Request 是否相同
  2. 檢查 local browser cache 是否有快取可以使用
  3. 伺服器檢查用戶端送來的請求與 ETag ,確認伺服器是否有新版檔案

真實的情況下,有可能 server 沒定義到 ETag,或有可能不使用 ETag,使用 content-md5 等等,狀況會有很多種,因此,必須根據 response header 中實際定義到的屬性來做判斷。

希望此文可以幫助新手不再踩坑,並設計出最符合自己需求的 cache policy。


ETag 額外補充

於 Facebook Front-End Developers Taiwan 社團分享文章後,有網友提出了一個很好的問題,因此對於 ETag 的計算我加上一些額外補充,儘管這跟前端不太有關係。

網友的問題如下:

如果 server 有負載平衡(load balance)的情況下有可能會產生不同的 Etag,這有什麼建議的作法去解決嘛?

第一個疑問:

問:為何“從 Github pull 下來同樣的檔案”,最後從不同 server 出來的 ETag 值不同 ?
答:因為不同的 server 計算 ETag 的方法不一樣

舉例,以 Nginx 1.13.2 來說,從下方 source code 可以看到,檔案的 last_modified_timecontent_length_n ,也就是最後更新時間 檔案內容長度 被用來計算 ETag

nginx-1.13.2 source code, file : ngx_http_core_module.c

但本地端檔案的 last_modified_time 是本地端完成 git pull 的時間,造成各個 server 因 git pull 產生檔案副本時間不同造成產生的 ETag 不同 。

要先知道使用的 server 怎麼計算這個值,才能去調整這個問題。