設計一個完美的http快取策略
- 2019 年 10 月 3 日
- 筆記
1、前言
作為一個前端,了解http快取是非常必要,它不僅是面試的必要環節,也更是實戰開發中必不可少需要了解的知識點,本文作者將從快取的概念講到如何在業務中設計一個合理的快取架構,帶你一步一步解開http快取的神秘面紗。
2、http快取定義
當客戶端向伺服器請求資源時,會先抵達瀏覽器快取,如果瀏覽器有「要請求資源」的副本,就可以直接從瀏覽器快取中提取而不是從原始伺服器中提取這個資源。http快取一般針對GET請求,POST請求一般不會快取,因為POST請求執行的任務都是對伺服器產生副作用或者非冪等性的任務,既然要改變伺服器資源,自然請求是要進入伺服器進行處理的。快取帶來的好處是巨大的,減少了http請求,自然也就減少的服務端壓力,並且增加了資源的訪問速度,但是胡亂使用快取,將會帶來資源的不及時更新,甚至資源更新錯位,災難也是巨大的。
http快取分為強快取和協商快取,下面帶大家一一了解兩種快取機制。
3、強快取
3.1、強快取定義
如果命中快取,直接從快取中拿數據,請求不會經過伺服器,返回的http狀態碼為200(from disk cache)

下面給一張流程圖來說明強快取的請求過程,為了方便假設瀏覽器存在一個快取資料庫(其實就是disk磁碟,快取數據存放的地方)。


仔細看上面的流程圖,強快取的最大特點就是在命中快取的情況下不會經過伺服器,而是直接返回。
3.2、Http頭Expires/Cache-Control設置強快取
Cache-Control裡面存在多個屬性來控制快取,設置強快取即設置資源的有效期,屬性為max-age.
下面使用Express給大家演示一下
app.get('/script1.js', function (req, res, next) { // res.header('Cache-Control', 'must-revalidate, max-age=600') // res.header('Content-Type', 'text/html') res.header('Cache-Control', 'max-age=20') res.sendFile(__dirname + '/script.js') })

Expires和max-age都是用於控制快取的生命周期。不同的是Expires指定的是過期的具體時間,例如Sun, 21 Mar 2027 08:52:14 GMT,而max-age指定的是生命時長秒數315360000。
區別在於Expires是 HTTP/1.0 的中的標準,而max-age是屬於Cache-Control的內容,是 HTTP/1.1 中的定義的。但為了想向前兼容,這兩個屬性仍然要同時存在。max-age是要優先於Expires的。
4、協商/對比快取
4.1、定義
協商快取與強制快取的不同之處在於,協商快取每次讀取數據時都需要跟伺服器通訊,並且會增加快取標識。在第一次請求伺服器時,伺服器會返回資源,並且返回一個資源的快取標識,一起存到瀏覽器的快取資料庫。當第二次請求資源時,瀏覽器會首先將快取標識發送給伺服器,伺服器拿到標識後判斷標識是否匹配,如果不匹配,表示資源有更新,伺服器會將新數據和新的快取標識一起返回到瀏覽器;如果快取標識匹配,表示資源沒有更新,並且返回 304 狀態碼,瀏覽器就讀取本地快取伺服器中的數據。
協商快取的最大特點是要經過伺服器驗證的,下面我們來講解協商快取的驗證流程。
第一次訪問:

再次訪問:

還是給一張流程圖來說明。(圖是盜的,協商快取也可以成為對比快取,圖中的對比快取就是協商快取)

4.2、協商快取如何驗證
第一次請求將response header的Last-Modified和Etag存起來,在第二次請求通過request header的If-Modified-Since和If-None-Match傳到服務端進行驗證,如果命中快取,返回304,不帶返回的數據,瀏覽器自動從快取中獲取數據資源,若未命中快取返回200,帶上數據資源。
** Last-Modified:**
伺服器在響應請求時,告訴瀏覽器資源的最後修改時間。

** If-Modified-Since:**
再次請求伺服器時,通過此欄位通知伺服器上次請求時,伺服器返回的資源最後修改時間。
伺服器收到請求後發現有頭If-Modified-Since 則與被請求資源的最後修改時間進行比對。
若資源的最後修改時間大於If-Modified-Since,說明資源又被改動過,則響應整片資源內容,返回狀態碼200;
若資源的最後修改時間小於或等於If-Modified-Since,說明資源無新修改,則響應HTTP 304,告知瀏覽器繼續使用所保存的cache。

** Etag / If-None-Match(優先順序高於Last-Modified / If-Modified-Since) **
** Etag:**
伺服器響應請求時,告訴瀏覽器當前資源在伺服器的唯一標識(生成規則由伺服器決定)。

** If-None-Match:**
再次請求伺服器時,通過此欄位通知伺服器客戶段快取數據的唯一標識。
伺服器收到請求後發現有頭If-None-Match 則與被請求資源的唯一標識進行比對,
不同,說明資源又被改動過,則響應整片資源內容,返回狀態碼200;
相同,說明資源無新修改,則響應HTTP 304,告知瀏覽器繼續使用所保存的cache。

4.3、Http頭如何設置協商快取
在強快取那一節說到使用Cache-Control的max-age來設置資源過期時間,那麼當max-age=0的時候呢,自然瀏覽器第一時間發現資源過期,request header就會帶著If-Modified-Since和If-None-Match去服務端驗證。
所以設置response header為:
Cache-Control: max-age=0
就可以觸發協商快取了,其實Cache-Control中還有兩個屬性都可以設置協商快取 must-revalidate和no-cache
must-revalidate的意義為必須進行驗證,但是它一般是和max-age一起使用的,不會單獨使用,
Cache-Control: must-revalidate, max-age=600
該頭資訊意義就是在資源有效期過後必須進行驗證, 與只設置max-age=600的區別是,前面一個是MUST,而後面一個是SHOULD,理論上來說它們的效果是一致的。
no-cache的意義千萬不能理解為不快取,下面兩段程式碼的意義是一樣的,即請求必須進行驗證,才可以使用快取資源,注意是MUST
Cache-Control: no-cache Cache-Control: must-revalidate, max-age=0
如果要不快取,每次都請求新的資源應該使用
Cache-Control: no-store
5、關於快取的Http頭總結
5.1、"no-cache", "no-store", "must-revalidate"
Cache-Control欄位可以設置的不僅僅是max-age存儲時間,還有其他額外的值可以填寫,甚至可以組合。主要使用的值有如下:
no-cache: 雖然字面意義是「不要快取」。但它實際上的機制是,仍然對資源使用快取,但每一次在使用快取之前必須(MUST)向伺服器對快取資源進行驗證。
no-store: 不使用任何快取
must-revalidate: 如果你配置了max-age資訊,當快取資源仍然新鮮(小於max-age)時使用快取,否則需要對資源進行驗證。所以must-revalidate可以和max-age組合使用Cache-Control: must-revalidate, max-age=60
有趣的事情是,雖然no-cache意為對快取進行驗證,但是因為大家廣泛的錯誤的把它當作no-store來使用,所以有的瀏覽器也就附和了這種設計。這是一個典型的劣幣驅逐良幣。
5.2、Expires VS. max-age
Expires和max-age都是用於控制快取的生命周期。不同的是Expires指定的是過期的具體時間,例如Sun, 21 Mar 2027 08:52:14 GMT,而max-age指定的是生命時長秒數315360000。
區別在於Expires是 HTTP/1.0 的中的標準,而max-age是屬於Cache-Control的內容,是 HTTP/1.1 中的定義的。但為了想向前兼容,這兩個屬性仍然要同時存在。
但有一種更傾向於使用max-age的觀點認為Expires過於複雜了。例如上面的例子Sun, 21 Mar 2027 08:52:14 GMT,如果你在表示小時的數字缺少了一個0,則很有可能出現出錯;如果日期沒有轉換到用戶的正確時區,則有可能出錯。這裡出錯的意思可能包括但不限於快取失效、快取生命周期出錯等。
5.3、Etag VS. Last-Modified
Etag和Last-Modified都可以用於對資源進行驗證,而Last-Modified顧名思義,表示資源最後的更新時間。
我們把這兩者都成為驗證器(Validators),不同的是,Etag屬於強驗證(Strong Validation),因為它期望的是資源位元組級別的一致;而Last-Modified屬於弱驗證(Weak Validation),只要資源的主要內容一致即可,允許例如頁底的廣告,頁腳不同。
根據RFC 2616標準中的13.3.4小節,一個使用HTTP 1.1標準的服務端應該(SHOULD)同時發送Etag和Last-Modified欄位。同時一個支援HTTP 1.1的客戶端,比如瀏覽器,如果服務端有提供Etag的話,必須(MUST)首先對Etag進行Conditional Request(If-None-Match頭資訊);如果兩者都有提供,那麼應該(SHOULD)同時對兩者進行Conditional Request(If-Modified-Since頭資訊)。如果服務端對兩者的驗證結果不一致,例如通過一個條件判斷資源發生了更改,而另一個判定資源沒有發生更改,則不允許返回304狀態。但話說回來,是否返回還是通過服務端編寫的實際程式碼決定的。所以仍然有操縱的空間。
5.4、max-age=0 VS. no-cache
max-age=0是在告訴瀏覽器,資源已經過期了,你應該(SHOULD)對資源進行重新驗證了;而no-cache則是告訴瀏覽器在每一次使用快取之前,你必須(MUST)對資源進行重新驗證。
區別在於,SHOULD是非強制性的,而MUST是強制性的。在no-cache的情況下,瀏覽器在向伺服器驗證成功之前絕不會使用過期的快取資源,而max-age=0則不一定了。雖然理論上來說它們的效果應該是一致的。
5.5、public VS. private
要知道從伺服器到瀏覽器之間並非只有瀏覽器能夠對資源進行快取,伺服器的返回可能會經過一些中間(intermediate)伺服器甚至甚至專業的中間快取伺服器,還有CDN。而有些請求返回是用戶級別、是私人的,所以你可能不希望這些中間伺服器快取返回。此時你需要將Cache-Control設置為private以避免暴露。
6、快取實戰
前面寫了很多快取的基礎知識,那麼如何設計一個可靠的快取規則,這個其實得根據你的實際需求而定。
比如某個資源永遠不會改變,比如某些第三方庫(一般都放CDN做優化了),或者某些圖片,比如百度的圖片,就讓它們永久快取著吧,設置一個最大的max-age
Cache-Control: max-age=31536000
其他的資源可根據下面這張決策樹來進行設置

7、memory cache

memory cache叫做記憶體快取,根據作業系統的常理,先讀記憶體,後讀硬碟。記憶體存儲較小,一般只存儲臨時性文件。
「記憶體快取」中主要包含的是當前文檔中頁面中已經抓取到的資源(預請求資源)。例如頁面上已經下載的樣式、腳本、圖片等。我們不排除頁面可能會對這些資源再次發出請求,所以這些資源都暫存在記憶體中,當用戶結束瀏覽網頁並且關閉網頁時,記憶體快取的資源會被釋放掉。
這其中最重要的快取資源其實是preloader相關指令(例如)下載的資源。總所周知preloader的相關指令已經是頁面優化的常見手段之一,而通過這些指令下載的資源也都會暫存到記憶體中。根據一些材料,如果資源已經存在於快取中,則可能不會再進行preload。
memory cache 機制保證了一個頁面中如果有兩個相同的請求 (例如兩個 src 相同的 ,兩個 href 相同的 )都實際只會被請求最多一次,避免浪費。
需要注意的事情是,記憶體快取在快取資源時並不關心返回資源的HTTP快取頭Cache-Control是什麼值,同時資源的匹配也並非僅僅是對URL做匹配,還可能會對Content-Type,CORS等其他特徵做校驗
這個應該是瀏覽器做的一種優化,快取也只是暫時的。
8、瀏覽器可能會限制Cache-Control頭無效

** 參考文章: **
https://zhuanlan.zhihu.com/p/28113197#comments
https://www.cnblogs.com/chenqf/p/6386163.html
