公司項目爲使用Angular,React,非單頁面應用。每個頁面要發多個請求,頁面加載緩慢。爲此,學習下HTTP Cache。
通過網絡請求獲取資源既慢又昂貴。大量的請求在服務端和客戶端之間往返,使得資源可用時間以及瀏覽器可處理它們的時間都有了延遲,同時用戶訪問的數據成本也會增加。因此,緩存和重用已獲取的資源是優化前端性能的一個關鍵點。
所謂緩存,就是將已經請求得到的內容放在一個就近的倉庫存放起來,下次請求可以不再向服務器請求,而是直接從這個緩存倉庫獲取。
Web緩存主要有以下幾個優勢:
- 減少網絡延遲,加快頁面響應速度。增強用戶體驗。
- 減少網絡帶寬的消耗
- 減輕服務器壓力
Web緩存的種類
Web緩存類型 | 描述 |
---|---|
數據庫緩存 | 當web應用關係複雜,數據表蹭蹭蹭往上漲時,可以將查詢後的數據放到內存中進行緩存,下次再查詢時,就直接從內存緩存中獲取,從而提高響應速度。 |
CDN緩存 | 當我們發送一個web請求時,CDN會幫我們計算去哪得到這些內容的路徑短且快。這個是網站管理員部署的,所以他們也可以將大家經常訪問的內容放在CDN裏,從而加快響應。 |
代理服務器緩存 | 代理服務器緩存,跟瀏覽器緩存性質類似,但是代理服務器緩存面向的羣體更廣,規模更大。它不只爲一個用戶服務,一般爲大量用戶提供服務,同一個副本會被重用多次,因此在減少響應時間和帶寬使用方面很有效。 |
瀏覽器緩存 | 每個瀏覽器都實現了 HTTP 緩存,我們通過瀏覽器使用HTTP協議與服務器交互的時候,瀏覽器就會根據一套與服務器約定的規則進行緩存工作。當我們在瀏覽器中點擊前進 和後退 按鈕時,利用的便是瀏覽器的緩存機制。 |
注意:
由於當前所有瀏覽器都默認附帶實現了HTTP緩存,這在給大家再來性能優化的同時,也可能附帶一些容易忽略的坑。
所以我們必須在服務端返回正確的HTTP header指令來告訴瀏覽器當前response的緩存策略以及其緩存時間。
Tips:
如果你在你的應用中是使用Webview來獲取和顯示內容,你可能需要添加其他配置以確保啓用瀏覽器緩存, 它的大小應該與你的用例匹配,緩存持久有效。查閱相關平臺文檔確保配置準確無誤。
當服務端返回響應時,同時附帶發出一個HTTP header的集合,描述了它的content-type(內容類型)、Content-length(長度)、緩存指令以及驗證令牌等等。如上圖,服務端返回了一個長度爲1024字節的response,並且提供了一個驗證令牌(“x234dff”
),ETag
用於在響應過期後驗證資源是否已被修改。
http響應頭中與緩存相關的指令字段:
- Expires : 告訴瀏覽器在該時間之前,可以直接從緩存中獲取資源,而無需向服務器獲取。
注意: 該時間是GMT 時間,而不是本地時間。
- Cache Control :
優先級高於Expires,如果同時設置了Cache Control 和Expires, Expires會被忽略。
- Last-Modified:
- If-Modified-Since:
- Etag:
一、使用ETags
驗證緩存的response
- 服務器使用http header
ETag
來傳遞驗證令牌; - 有了驗證令牌,可以有效的進行資源更新的檢查; 如果資源沒有更改,則不會傳輸任何數據。
例如:
瀏覽器發起上一次的get請求已經超過了120s(Cache-Control:max-age=120)
,並且瀏覽器發起對同一資源的新請求。
首先,瀏覽器檢查本地緩存並找到先前的響應。但是響應已過期,無法使用之前的response。
此時,瀏覽器可以發起新的請求並獲取新的完整的響應結果,但是效率相對低下,因爲資源並沒有被修改,沒有必要重複下載已經存在緩存中的資源。
這就是ETag
會解決的問題。服務器生成並返回任意ETag
,該ETag
通常是文件內容的散列或其他指紋。客戶端不需要知道ETag
是如何生成的;它只需要在下一個請求時將其發送到服務器。如果ETag
的值仍然相同,則資源未更改,可以跳過下載,訪問本地緩存資源。
如上圖,客戶端在HTTP請求頭中自動提供了If-None-Match
,這個If-None-Match
就是上次請求服務器時,服務器返回的Etag
。服務端根據當前所訪問的資源檢查ETag
。如果沒有改變,則服務端返回304 Not Modified
,告訴瀏覽器當前所訪問的資源並沒有發生修改,可以繼續用瀏覽器中的緩存,並將緩存時間重新計時120s。此時,瀏覽器不會在下載response,可以節省時間和帶寬。
作爲Web開發人員,如何進行有效的重新驗證呢?在這裏,瀏覽器已經幫我們完成了全部工作。瀏覽器會自行檢測先前是否已經指定驗證令牌,並在發出請求時將驗證令牌附加到Http請求頭中,並根據從服務端收到的response更新緩存時間戳。唯一需要做的事情是需要確保服務端會提供必要的ETag
令牌。
Tips:
Github項目 [h5bp/server-configs](https://github.com/h5bp/server-configs)包含了流行服務器的配置文件示例代碼,並配有詳細的註釋。你可以在其中找到自己所使用的服務器,並查找相應的配置來確認自己的服務器配置是否準確。
二、Cache-Control
- 每個資源都可以通過http header
Cache-Control
來定義其緩存策略。 Cache-Control
指令指明瞭誰來緩存響應、緩存條件以及緩存時間。
從性能優化的角度來看,最好的請求是不需要與服務端進行交互——一個本地的響應副本可以消除所有的網絡延時並避免數據傳輸時的數據費用。 因此,HTTP規範允許服務器返回[Cache-Control]
(https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9)指令,該指令用來控制瀏覽器及其他中間緩存件如何緩存單個響應以及緩存多長時間。
Tips:
Cache-Control標頭被定義爲HTTP / 1.1規範的一部分,並且取代了原先用於定義response緩存策略的
Expire等,現在所有的現代瀏覽器都支持HTTP Cache-Control。
2.1 “no-cache”和“no-store”
no-cache:有緩存,但是不直接使用緩存,需要經過校驗。
如果資源已經發生更改,在沒有與服務端進行校驗前,瀏覽器不能用前面返回的response用於滿足對同一URL的後續請求。如果瀏覽器提供了正確的ETag
,當配置了no-cache
時,客戶端會先發出請求向後端驗證資源是否發生更改,如果資源未更改,則可以取消下載。
no-store:完全沒有緩存,所有的資源都需要重新發請求
當服務端對資源設置了no-store
時,不允許瀏覽器和所有中間層緩存response
。。例如:一些類似於銀行及其他隱私數據不適合緩存,每次的用戶請求都需要發送到服務端,下載全部的response。
2.2 “public” 和 “private”
public
如果response被標記爲“public
”,那麼,即使有與其相關的HTTP認證信息或者返回的response是不可緩存的status code,它依然可以被緩存。大多數情況下,public
並不是必需的,因爲其他具體指示緩存的信息,如max-age
會表明當前的response在任何情況下都是要緩存的。
private
相比之下,瀏覽器可以緩存private
的response。但是,這些響應通常只用於單個用戶,因此不允許其他中間緩存對齊進行緩存。例如:一個用戶的瀏覽器可以帶有用戶私有信息的HTML頁面,但是CDN無法緩存頁面。
2.3 max-age
max-age
指令指定了允許重用緩存的response的最長時間(以秒爲單位)。例如,max-age=60
表示response可以緩存,並且在接下來的60s內可以被重用,無需發出新的request請求。
三、 定義最佳的Cache-Control策略
遵循上面的流程圖爲應用程序中使用的特定資源或一組資源制定最佳緩存策略。理想的情況下,我們應該在客戶端上在儘可能長的時間中緩存儘可能多的響應,併爲每個響應提供驗證令牌(ETag
),從而實現有效的重新驗證。
Cache-Control指令和解釋
指令 | 解釋 |
---|---|
max-age=86400 | Response can be cached by browser and any intermediary caches (that is, it’s “public”) for up to 1 day (60 seconds x 60 minutes x 24 hours). |
private, max-age=600 | Response can be cached by the client’s browser only for up to 10 minutes (60 seconds x 10 minutes). 不能被代理服務器緩存。 |
public | 響應可以被任何緩存區緩存。 |
no-cache | 瀏覽器對請求回來的response做緩存,但是每次在向客戶端(瀏覽器)提供響應數據時,緩存都要向服務器評估緩存響應的有效性 |
no-store | Response is not allowed to be cached and must be fetched in full on every request. 禁止一切緩存。 |
根據HTTP檔案,排名前300,000(按照Alexa)的網站中,瀏覽器可以緩存幾乎一半的下載響應,這對於網頁的重複瀏覽和訪問來說是個巨大的節省。當然,這並不意味着你的應用程序可以緩存50%的資源。有些網站的靜態資源幾乎不會變動,可能可以緩存超過90%的資源;其他網站可能有很多私有的或者是時間敏感的數據完全不能使用緩存。
審覈自己的頁面,確認哪些資源是可以緩存的並且確保它們返回了合適的Cache-Control
和Etag
。
四、作廢和更新緩存的response
- 本地緩存的response可以一直被使用,直到資源
expires
; - 在URL中嵌入一個文件內容的指紋可以強制客戶端不使用緩存,更新
response
; - 每個應用程序都需要定義自己的緩存層次結構來獲得最佳性能。
所有從瀏覽器發出去的請求首先要路由到瀏覽器緩存中,檢驗是否存在可以用來完成該請求的有效緩存。如果有匹配的緩存,則response
從緩存中讀取,從而消除網絡延遲和傳輸引起的數據成本。
如何作廢或者更新一個緩存的response
?
例如: 你已經告知訪問者緩存一個css
樣式表24h(max-age = 86400
),但是開發者剛剛提交了你希望向所有用戶提供的更新。這時該如何通知訪問者更新原有的緩存呢?此時,如果不改變資源的URL,無法更新資源。因爲瀏覽器認爲當前的緩存尚未過期。
瀏覽器緩存響應之後,直到根據max-age
或者expire
指示,緩存已過期或者緩存被清理調,都會使用緩存的資源。因此,在頁面構建時,或者說在訪問某個網站時,不同的用戶可能使用不同版本的資源。剛剛獲取資源的用戶使用的是最新版本,但緩存了早期資源(仍然有效
)的用戶依然使用舊版本的資源。
如何充分利用“客戶端緩存”和“快速更新”?
在資源內容更改時,改變資源的URL,強制用戶下載新的response
。通常,我們可以通過在文件名中嵌入版本號或者其他文件指紋來實現改變URL,例如:style.x234dff
。
定義每個資源的緩存策略(定義緩存結構層次),不僅可以控制每個資源的緩存時長,還可以控制訪問者獲取和查看新版本的速度。
如上圖中的例子:
- HTML文件被標記爲
no-cache
,意味着瀏覽器對於每個請求都會重新驗證文檔,如果資源內容發生改變,就會拉取最新版本。 此外,在HTML標記中,在css和javascript資源的URL中嵌入了指紋:如果這些文件的內容發生更改,HTML文件也會更改,從而加載一個HTML 響應的新副本。 - CSS允許瀏覽器和其他中間緩存(例如:CDN)進行緩存,過期時間爲1年。其實,我們也可以使用
far future expires
of 1 year, 因爲我們在文件名中嵌入了文件指紋,如果CSS文件發生更新,其請求的URL也會發生改變。 - js文件的過期時間也設置爲1年,但是標記爲
private
,可能是因爲它裏面包含了一些CDN不應緩存的私有用戶數據。 - 圖片文件中沒有加版本和獨一無二的hash指紋,直接進行緩存。緩存時間設置爲1天。
結合ETag
、Cache-Control
和獨一無二的URL
,可以實現最好的緩存策略:更長的過期時間、控制特定資源的緩存以及緩存位置、按需更新。
五、緩存設置清單
世界上沒有最好的緩存策略。根據你的流量模式、所提供的數據類型以及應用程序對於資源新鮮度的特定需求,我們需要對每個資源以及整體的“緩存層次結構”制定適當的緩存策略。
制定緩存策略時的一些提示和技巧:
- 使用一致的URL(
hash/version
):如果對於內容完全相同的資源使用不同的URL,那麼這個資源會不停地被獲取和存儲。Tips:URL需要區分大小寫。
- 確保服務端提供了驗證令牌(
ETag
): 驗證令牌的存在避免了在服務端資源沒有任何修改時傳輸完全相同的字節內容。 - 確定哪些資源可以被中介緩存(
public/private
):對於所有用戶都相同的response可以在CDN和其他中介緩存中緩存。 - 確定每個資源的最佳緩存時長(
max-age
):不同的資源可能具有不同的新鮮度要求。審覈並確認每個資源合適的max-age
。 - 確定當前網站的最佳緩存層次結構:結合資源的
URL
和資源內容的指紋以及HTML的短緩存或者no-cache
, 我們可以控制客戶端獲取和更新資源的速度。 - 減少混亂:有些資源的更新頻率要高於其他資源。如果有一部分資源(比如:一個javascript函數或者一組CSS樣式)需要經常更新,可以考慮將這一部分代碼作爲單獨的文件。這樣做,其餘部分的資源(例如:不經常更新的庫代碼)就可以從緩存中提取。當獲取更新時,使得需要下載內容的數量最小化