前端效能優化 之 瀏覽器快取

wfz發表於2018-09-28

作為開發人員大家都知道,從網路上獲取資源成本比較高,客戶端需要和服務端要進行多次通訊,如果能有效利用快取,可以極大提高 web 應用的效能,所以有必要詳細瞭解一個關於快取的各個細節。

為防止出現理出現解上的偏差,在開始之前我們約定關於快取處理指的是:當使用者打個某個網址或者應用 以後 把它關閉,然後 再次開啟 的的情況。

如果使用者主動點選了 重新整理 或者 強制重新整理(CTRL+F5) 的情況在最後面再詳細說。

概念篇

瀏覽器快取分兩個型別:非驗證性快取驗證性快取

非驗證性快取:瀏覽器根據過期時間來判斷是否使用快取,如果在有效期內,直接從瀏覽器快取中讀取檔案,不發生http請求,涉及到的 header 欄位有 Cache-Controlexpirespragma

驗證性快取:給服務端傳送請求時,在 header 裡附帶條件,服務端在處理請求時根據指定條件做出判斷,如果符合條件則返回一個 304 狀態碼並返回空的 body ,瀏覽器在接收到 304 狀態碼後得知本地快取依然有效,直接從本地快取讀取;如果條件為假則返回 200 狀態碼並返回指定資源。涉及到的 header 欄位有 etaglast-modified

從以上內容可知,非驗證性快取最優,他從本地讀取,甚至都不會發生網路請求;其次是驗證性快取,他會產生網路請求,但如果快取可用,它返回的 body 為空,資料傳輸量也是非常小。

瀏覽器在判斷快取時的順序是:

非驗證性快取 > 驗證性快取

下面開始逐個來講,會涉及到一些伺服器方面的知識,如果對 nginx 不熟,推薦看一下這篇文章:前端工程師學習 Nginx 入門篇

非驗證性快取

非驗證性快取 主要以 Cache-Controlexpirespragma 這三個訊息頭控制。由於 pragma 是 HTTP/1.0 中的規範,它在響應中的行為沒有確切規範,而且他可以被 Cache-Control 覆蓋,所以這裡我們不說了,只看 Cache-Controlexpires

Cache-Control

如果在 nginx 的配置檔案裡有如下配置:

# nginx.conf
add_header Cache-Control max-age=20;
複製程式碼

這個指令的含義是指定資源的過期時間是 20s ,在 20s 內,如果瀏覽器對這個資源有重複請求,將不會產生 http 請求,直接從瀏覽器快取中讀取。請求響應頭如下:

HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 28 Sep 2018 14:10:36 GMT
Content-Type: text/html
Content-Length: 755
Last-Modified: Thu, 27 Sep 2018 22:44:02 GMT
Connection: keep-alive
Cache-Control: max-age=20
Accept-Ranges: bytes
複製程式碼

其他引數先不看,可以看到的是 max-age=20 定義了過期時間是 20s,同時可以在瀏覽器和服務端 log 中驗證,確實沒有發生 http 請求。

Cache-control 用的最多的是 max-age ,但它還有其他很多指令,分別代表不同的含義:

Cache-control: must-revalidate
Cache-control: no-cache
Cache-control: no-store
Cache-control: no-transform
Cache-control: public
Cache-control: private
Cache-control: proxy-revalidate
Cache-Control: max-age=<seconds>
Cache-control: s-maxage=<seconds>
複製程式碼

感興趣的朋友可以在 MDN Cache-control 上詳細瞭解。

expires

現在重新配置 nginx 如下:

# nginx.conf
#add_header Cache-Control max-age=20;
add_header expires 'Thu, 27 Sep 2019 22:44:02 GMT';
複製程式碼

註釋掉 Cache-Control,新增一個 expires 訊息頭,值是未來的某一時刻,這時再訪問頁面,如果快取命中,響應頭裡會有如下資訊:

HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 28 Sep 2018 14:45:15 GMT
Content-Type: text/html
Content-Length: 755
Last-Modified: Thu, 27 Sep 2018 22:44:02 GMT
Connection: keep-alive
expires: Thu, 27 Sep 2019 22:44:02 GMT
Accept-Ranges: bytes
複製程式碼

從響應資訊頭中可以明確看出過期時間,在這個截至時間內訪問都不會產生新的 http 請求,直接從瀏覽器快取中讀取資源。

Cache-Control 優先順序高於 expires

這個時候如果我們重新編輯 nginx 配置檔案如下:

# nginx.conf
add_header Cache-Control max-age=20;
add_header expires 'Thu, 27 Sep 2019 22:44:02 GMT';
複製程式碼

再訪問如可以看到如下響應頭資訊:

HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 28 Sep 2018 14:49:50 GMT
Content-Type: text/html
Content-Length: 755
Last-Modified: Thu, 27 Sep 2018 22:44:02 GMT
Connection: keep-alive
Cache-Control: max-age=20
expires: Thu, 27 Sep 2019 22:44:02 GMT
Accept-Ranges: bytes
複製程式碼

由於 Cache-Control 優先順序高於 expires ,在實際測試過程中可知快取在 20s 後就過期了。

expires 是http 1.0中定義的訊息頭,Cache-Control 是 http 1.1 中定義的訊息頭,如果他們同時存在 Cache-Control 會覆蓋 expires,而且 expires 返回的時間是伺服器時間,如果伺服器時間與客戶端時間不一致,會造成很大誤差,並且 http 1.1 已經被向乎所有瀏覽器支援,所以使用 Cache-Control 就好。

驗證性快取

看完了非驗證性快取瞭解到他對靜態資源非常重要,可以極大節省頻寬,提升 web 應用效能。但有些資源有一定的時效性,需要經常去伺服器驗證是否有更新,如 html 、 api 介面等,這個時候就需要用到驗證性快取。

如前所述,驗證性更新需要向服務端傳送一個請求,如果服務端判斷沒有更新,返回一個 304 狀態碼並返回一個空的 body 資訊體,瀏覽器可以直接從本地快取讀取資源。如果有更新,返回 200 狀態碼並將最新資源一併返回。

驗證性快取主要由 last-modifiedetag 這兩個訊息頭控制,接下來我們依次來看。

last-modified

last-modified 是 nginx 預設開啟的,所以不用手動去配置它。

由於 非驗證性快取 的優先順序要高於 驗證性快取,所以測試的時候需要將他們設為無效,要不然看不到效果:

# nginx.conf
add_header Cache-Control max-age=0;
#add_header expires 'Thu, 27 Sep 2019 22:44:02 GMT'; # Cache-Control優先順序較高,設定一個就好
複製程式碼

現在再看,如果再次訪問,請求頭會帶上類似如下欄位:

...
If-Modified-Since: Thu, 27 Sep 2018 22:37:45 GMT
...
複製程式碼

這個時候再測試,對於 資源未更新 的情況,響應頭如下:

HTTP/1.1 304 Not Modified
Server: nginx/1.12.2
Date: Fri, 28 Sep 2018 15:29:06 GMT
Last-Modified: Thu, 27 Sep 2018 22:37:45 GMT # 明確標示最後修改時間
Connection: keep-alive
Cache-Control: max-age=0
複製程式碼

也能看到瀏覽器端是直接從快取中取的內容。

對於 資源發生過更新 的情況,響應頭如下:

HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 28 Sep 2018 15:29:06 GMT
Content-Type: application/javascript
Content-Length: 4770
Last-Modified: Fri, 28 Sep 2018 15:29:03 GMT # 明確標示最後修改時間
Connection: keep-alive
Cache-Control: max-age=0
Accept-Ranges: bytes
複製程式碼

見下圖:

status

last-modifiedif-modified-since 是成對出現的,分別的作用是:

  • last-modified 在響應頭裡,伺服器告訴瀏覽器,這個資源的最後修改時間是什麼
  • if-modified-since 在請求頭裡,告訴伺服器我所請求的這個資源最後修改時間是什麼。伺服器根據這個值來判斷,如果這個值和服務端這個資源現有的值一致,直接返回 304 和空的 body,如果和服務端現有的值不一致(資源已經更新),則返回 200 和最新資源。

根據這兩個時間,伺服器和瀏覽器就能夠決定資源是否是最新的,是否可以使用本地快取。

etag

ETag HTTP 響應頭是資源的特定版本的識別符號,它和 last-modified 類似,都是為了實現資源的驗證性快取,但 etag 精度更高( last-modified 只能精確到秒),同時 etag 還能避免“空中碰撞”,詳細的解釋可以看 MDN 的 Etag 介紹。

下面直接來看他的實現:

# nginx.conf
etag on; # 手動開啟 etag
add_header Cache-Control max-age=0;
#add_header expires 'Thu, 27 Sep 2019 22:44:02 GMT'; 
add_header Last-Modified ''; # 為了測試 etag 的效果,將 last-modified 設為無效
複製程式碼

現在再看,如果再次訪問,請求頭會帶上如下欄位:

...
If-None-Match: "5bad5bb9-13a3f"
...
複製程式碼

這個時候再測試,對於 資源未更新 的情況,響應頭如下:

HTTP/1.1 304 Not Modified
Server: nginx/1.12.2
Date: Fri, 28 Sep 2018 22:43:11 GMT
Connection: keep-alive
ETag: "5bad5bb9-13a3f"
Cache-Control: max-age=0
複製程式碼

可以看到瀏覽器端是直接從快取中取的內容。

對於 資源發生過更新 的情況,響應頭如下:

HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 28 Sep 2018 22:43:11 GMT
Content-Type: application/javascript
Content-Length: 4770
Connection: keep-alive
ETag: "5baeae7a-12a2"
Cache-Control: max-age=0
Accept-Ranges: bytes
複製程式碼

可以看到伺服器將最新的內容傳輸給瀏覽器,並返回 200 code

etagif-none-match 是成對出現的,

  • etag 是伺服器根據一定規則生成的資源‘指紋’,傳遞給客戶端,客戶端將其與快取一起儲存
  • if-none-match 是客戶端在向服務端請求指定資源時,將本地的 etag 值通過資訊頭傳遞給服務端,服務端與其當前版本的資源的ETag進行比較,如果兩個值匹配(即資源未更改),伺服器將返回不帶任何內容的304未修改狀態,告訴客戶端快取版本可用。如果 etag 值匹配不成功,返回 200 code 和資源內容。

使用者主動重新整理行為

當使用者主動點選了 重新整理 或者 強刷重新整理,瀏覽器會在請求頭資訊裡附上不同的欄位,來告訴伺服器如何處理這個行為。

使用者點選 重新整理

當使用者點選重新整理時,瀏覽器在請求頭裡會加上如下欄位:

If-Modified-Since: Fri, 28 Sep 2018 22:43:06 GMT # 如果開啟了 If-Modified
If-None-Match: "5baeae7a-12a2" # 如果開啟了 etag
Cache-Control: max-age=0
複製程式碼

這時即便 Cache-Control 設定了更大的值,也不會從本地快取中直接讀取,而是要傳送一條新的請求去伺服器驗證資源是否有更新,所以這個時間就跳過了第一階段的 非驗證性快取,進入 驗證性快取。

使用者點選 強制重新整理

當使用者點選強制重新整理時,瀏覽器在請求頭裡會加上如下欄位:

Pragma: no-cache
Cache-Control: no-cache
複製程式碼

可以看到,即便 Cache-Control 設定了更大的值,也不會從快取中直接讀取,而且不會傳送 If-Modified-SinceIf-None-Match ,也就是說伺服器得不到資源的最後更新時間和 etag 值,無論如何都會返回最新的資源。

所以當使用者 強制重新整理 時,瀏覽器主動跳過了 非驗證性快取 和 驗證性快取,直接從服務端獲取最新資源。

這也是為什麼需求方找我們看問題的時候,我們總是喜歡讓他們強制重新整理的原因...

參考文件

相關文章