本文可以配合本人錄製的視頻一起食用
目的
通常説到瀏覽器緩存,大多是和性能優化有關,使用緩存,通常是兩個主要目的,第一是提高訪問速度,第二是減少網絡IO消耗。
當合理配置了緩存,可以得到提升用户體驗、減輕服務器負擔、節省帶寬等效果,這是一種效果顯著的前端性能優化手段。
四個方面
瀏覽器緩存機制涉及四個方面,按照獲取資源時請求的優先級排序如下:
- Memory Cache
- Service Worker Cache
- HTTP Cache
- Push Cache
其中最後一個Push Cache是HTTP2的新特性
緩存策略
通常我們面試時説的瀏覽器緩存原理,指的是緩存策略。
緩存策略分為兩類:強緩存和協商緩存,通過設置HTTP HEADER來實現。
強緩存
強緩存優先級較高,在命中強緩存失敗的情況下,才會走協商緩存。
當我們發起請求,去請求一個資源時,瀏覽器會去讀取HTTP Header中的expires或Cache-control,來判斷目標資源是否命中強緩存。
使用expires還是Cache-control和我們的HTTP協議版本有關,當使用1.0版本就根據expires進行判斷,當使用1.1就根據Cache-control進行判斷。
如果命中了強緩存,就直接從緩存中獲取資源,不與服務端發生通信。此時返回的HTTP狀態碼為200,但會有from memory cache的標識。
【判斷資源是否過期
那瀏覽器是如何根據expires和Cache-control進行判斷的呢?
首先看expires,expires是一個時間戳,指的是資源過期時間,當我們試圖再次從服務器請求同一份資源時,瀏覽器就會先對比本地時間和expires的值,如果本地時間小於expires設置的過期時間,就直接從緩存中獲取資源。
因為expires的使用依賴於本地時間,這就會存在問題,不同的本地時間會使緩存的失效時間無法保證,也就是服務端和客户端會存在時差。
因此HTTP 1.1增加了Cache-control作為expires的完全替代方案,現在依然使用expires的唯一目的是為了向下兼容。兩者同時使用時,Cache-control的優先級更高。
在Cache-control中我們可以設置max-age來控制資源的有效期,這是一個時間長度,規避了時間戳帶來的問題,客户端會記錄請求到資源的時間點,以此作為相對時間的起點,確保參與計算的兩個時間節點都來源於客户端。
協商緩存
如果強緩存命中失敗,瀏覽器就需要向服務器去詢問緩存的相關信息,進而判斷是重新請求、下載完整的響應,還是從本地中獲取緩存的資源,此時就進入了協商緩存的階段。
如果我們不想直接使用本地緩存,也可以設置Cache-Control: no-cache直接進入協商緩存的階段。
協商緩存,從字面上可以理解為:根據客户端和服務器的協商結果,來判斷是否使用本地緩存。
如果服務器提示目標資源未改動(Not Modified),資源會被重定向到瀏覽器緩存,這種情況下網絡請求對應的狀態碼是304。
【判斷資源有效性
那麼服務器是如何去判斷資源是否變動呢?
有兩種判斷方式:Last-Modified和Etag
首先看Last-Modified,它是一個時間戳,如果啓用了協商緩存,它會在請求時隨着response headers返回。
Last-Modified:
之後我們每次請求時,請求頭request headers中就會帶上一個時間戳字段,叫If-Modified-Since,它的值就是之前response返回的Last-Modified的值。字面上的意思就是在告訴服務器:在這個時間之後,資源如果發生變化,就要返回新的資源
服務器接收到這個字段後,就會與該資源在服務器上的最後修改時間進行比對,兩者是否一致,從而判斷資源是否發生變化。
-
如果發生變化
會返回一個完整的響應。並在響應頭response headers中返回新的Last-Modified的值
-
如果沒有發生變化
就會返回一個狀態碼為304的響應,提示瀏覽器使用本地緩存
Last-Modified和If-Modified-Since配合使用,看上去沒什麼問題,但實際存在一些弊端,比如:
-
編輯了文件,但文件內容實際沒有變化的情況
我可能修改了一個文件,然後又撤銷了修改,雖然文件內容沒變,但資源的最後修改時間被更新了。
導致需要重新將資源返回給客户端,無法充分利用緩存
-
第二種情況,修改文件的速度過快,導致客户端使用了過期緩存
因為If-Modified-Since只能檢查以秒為最小計量單位的時間差,如果在不可感知的時間內修改了資源,就會使客户端使用過期的資源
為了彌補Last-Modified的不足,就出現了Etag
Etag是服務器為每個資源生成的唯一標識字符串,基於文件內容編碼,能夠精準感知文件內容變化
首次請求時,客户端會在response headers獲得一個標識字符串,類似:
Etag: "dec8d6f46497a9c6b4dff4e237cabe5d"
再次請求時,請求頭request headers裏會帶上一個與Etag值相同的、名為If-None-Match的字段,字面上的意思就是在請求時告訴服務器:資源的Etag如果不一致,就要返回新的資源
If-None-Match的值就是供服務器進行比對:
If-None-Match: "dec8d6f46497a9c6b4dff4e237cabe5d"
Etag很好的彌補了Last-Modified的短板,但是也存在弊端,因為生成Etag需要服務器付出額外的開銷,這會影響服務器的性能,這是啓用Etag時需要考慮到的問題。
當Etag和Last-Modified同時存在時,Etag的優先級更高,因為它在感知文件變化上比Last-Modified更準確
如何配置?
瞭解了緩存策略,那在面對一個具體的緩存需求時,我們該如何配置呢?
網上這一張應該挺多人看到過的圖,我也直接拿過來用,就是根據你項目的需求進行判斷
-
考慮這個響應是否是可複用的,也就是是否使用緩存
-
如果是No,也就是不可複用,就設置no-store,拒絕一切形式的緩存,包括客户端和服務器的緩存
Cache-Control: no-store -
如果是可複用響應
-
並且需要每次都進行資源有效性驗證,就設置no-cache,會跳過強緩存判斷,直接進入協商緩存階段,向服務器進行資源有效性驗證
Cache-Control: no-cache -
考慮是否可被代理服務器緩存
- 如果只能被瀏覽器緩存,就設置為private,這也是默認值
Cache-Control: private- 如果即可被瀏覽器緩存,也能被代理服務器緩存,就設置為pulic
Cache-Control: public -
繼續我們可以考慮設置緩存有效時間,以保證緩存的有效性
- 設置代理服務器緩存的有效時間,使用s-maxage,單位為秒
Cache-Control: public s-maxage=30- 設置瀏覽器緩存的有效時間,使用max-age,單位也為秒
Cache-Control: max-age=30s-maxage優先級高於max-age,如果s-maxage未過期,則向代理服務器請求其緩存內容,只針對public緩存有效。
- 最後設置協商緩存需要用到的ETag、Last-Modified等參數
-
-
這個圖大概就是這麼一個流程。
實操
以上大多都是我查到的一些資料,接下來做一些簡單的配置,以科學的精神驗證一部分內容。
下面的測試在nginx和Chrome中進行。
當不做配置時
location /cache-demo {
root html;
index index.html;
}
-
第一次訪問頁面
// 響應碼 200 Status Code: 200 OK // 響應頭裏加上了Last-Modified和Etag Last-Modified: Thu, 27 Jul 2023 02:47:55 GMT ETag: "64c1dadb-e3" // 第一次請求頭裏沒有`If-Modified-Since`和`If-None-Match` -
再次請求,status變成了304,説明資源內容沒有更改
// 響應碼304 Status Code: 304 Not Modified // 響應頭裏Last-Modified和Etag和之前一次是一樣的 Last-Modified: Thu, 27 Jul 2023 02:47:55 GMT ETag: "64c1dadb-e3" // 這一次請求頭裏帶上了`If-Modified-Since`和`If-None-Match` If-Modified-Since: Thu, 27 Jul 2023 02:47:55 GMT If-None-Match: "64c1dadb-e3" -
此時我們去修改資源內容,再去重新訪問,響應碼重新變成了200
// 響應 HTTP/1.1 200 OK Last-Modified: Thu, 27 Jul 2023 03:50:18 GMT ETag: "64c1e97a-e3" // 請求頭 If-Modified-Since: Thu, 27 Jul 2023 02:47:55 GMT If-None-Match: "64c1dadb-e3"可以看到響應頭中的Etag和請求頭中的If-None-Match不一致,Last-Modified和If-Modified-Since也不一致了,所以就返回了新的資源內容
配置no-store
接下來先做第一種配置,Cache-Control設置為no-store
add_header 'Cache-Control' 'no-store';
-
第一次訪問
// 響應 HTTP/1.1 200 OK Last-Modified: Thu, 27 Jul 2023 03:50:18 GMT ETag: "64c1e97a-e3" // 在響應頭中看到了no-store的配置 Cache-Control: no-store // 請求頭裏沒有`If-Modified-Since`和`If-None-Match` -
再次請求,可以看到status還是200,説明從服務器重新獲取資源內容了
// 響應 HTTP/1.1 200 OK Last-Modified: Thu, 27 Jul 2023 03:50:18 GMT ETag: "64c1e97a-e3" Cache-Control: no-store // 請求 // 請求頭裏沒有`If-Modified-Since`和`If-None-Match`,只有Cache-Control:max-age=0 Cache-Control: max-age=0 - 再請求幾次結果也是一樣的
配置max-age
修改配置max-age=240,也就是4分鐘內如果命中緩存,就使用緩存
add_header 'Cache-Control' 'max-age=240';
-
第一次請求資源
// 響應頭 HTTP/1.1 200 OK Last-Modified: Thu, 27 Jul 2023 04:13:31 GMT ETag: "64c1eeeb-e3" Cache-Control: max-age=240 // 請求 // 請求頭裏沒有`If-Modified-Since`和`If-None-Match` -
四分鐘內再次請求,status是304
// 響應頭 HTTP/1.1 304 Not Modified Last-Modified: Thu, 27 Jul 2023 04:13:31 GMT ETag: "64c1eeeb-e3" Cache-Control: max-age=240 // 請求頭 Cache-Control: max-age=0 If-Modified-Since: Thu, 27 Jul 2023 04:13:31 GMT If-None-Match: "64c1eeeb-e3"似乎沒有直接使用本地緩存,默認的請求頭的max-age=0,表示不使用強緩存,但允許協商緩存,所以返回響應碼是304
使用Chrome插件ModHeader來修改請求頭
Cache-Control: max-age=240,還是304?懷疑是緩存不夠用,因為理論上而言,應該使用本地緩存 -
HTML增加外部腳本的引用,請求腳本時可以命中緩存
試了幾次修改HTML,還是不行,給HTML增加外部的js腳本,發現腳本可以被緩存,顯示了from memory cache。可能是瀏覽器默認的策略,請求頁面的時候默認為
no-cache直接進入協商緩存階段;請求其他類型資源的時候才會優先去匹配本地緩存。Request URL: http://localhost:8080/cache-demo/main.js Status Code: 200 OK (from memory cache)此時設置
max-age=240,發現腳本沒有按照我們預期設置的max-age過期而重新獲取新的資源,這應該是Chrome本身對資源加載的一個優化,以達到充分利用本地緩存的目的,這也解釋了有些情況下,前端代碼重新部署後,無法加載到最新內容的原因,一來HTML內容沒有更改,所以默認的協商緩存返回304,讀取了本地資源;二來HTML引用的js腳本也在本地可以找到緩存,就沒有向瀏覽器發起請求。最終導致讀取到的是舊的資源內容,需要等待幾分鐘才能讀取到新的內容。為了解決這個問題,可以在nginx配置顯式的聲明
no-cache:add_header 'Cache-Control' 'no-cache, max-age=240';這樣請求js腳本就會直接進入協商緩存,讀取到新的資源內容。
結論
根據這個測試結果,為了保證內容的時效性,建議給資源所在服務器增加no-cache的配置,並且給HTML頁面所在服務器增加no-store的配置,因為大多Vue或者react單頁應用打包構建出來的HTML頁面內容很少,往往只有一個div,配置了no-store可以保證請求到最新的資源,因為頁面內容極少,正常情況下資源返回也非常快。