0 前言
在前端開發中,效能一直都是被大家所重視的一點,然而判斷一個網站的效能最直觀的就是看網頁開啟的速度。其中提高網頁反應速度的一個方式就是使用快取。快取技術一直一來在WEB技術體系中扮演非常重要角色,是快速且有效地提升效能的手段。
一個優秀的快取策略可以縮短網頁請求資源的距離,減少延遲,並且由於快取檔案可以重複利用,還可以減少頻寬,降低網路負荷。
所以,快取技術是無數WEB開發從業人員在工作過程中不可避免的一大問題。在產品開發的時候我們總是想辦法避免快取產生,而在產品釋出之時又在想策略管理快取提升網頁的訪問速度。瞭解瀏覽器的快取命中原理,是開發WEB應用的基礎,本文著眼於此,學習瀏覽器快取的相關知識,總結快取避免和快取管理的方法,結合具體的場景說明快取的相關問題。希望能對有需要的人有所幫助。
1 WEB快取體系
在實際WEB開發過程中,快取技術會涉及到不同層、不同端,比如:使用者層、系統層、代理層、前端、後端、服務端等,每一層的快取目標都是一致的,就是儘快返回請求資料、減少延遲,但每層使用的技術實現是各有不同,面對不同層、不同端的優劣,選用不同的技術來提升系統響應效率。所以,我們首先看下各層的快取都有哪些技術,都快取哪些資料,從整體上,對WEB的快取技術進行了解,如下圖所示:
本篇文章重點講的就是上面紅色框部分快取內容。
2 認識瀏覽器快取
當瀏覽器請求一個網站的時候,會載入各種各樣的資源,比如:HTML文件、圖片、CSS和JS等檔案。對於一些不經常變的內容,瀏覽器會將他們儲存在本地的檔案中,下次訪問相同網站的時候,直接載入這些資源,加速訪問。
這些被瀏覽器儲存的檔案就被稱為快取(不是指Cookie或者Localstorage)。
那麼如何知曉瀏覽器是讀取了快取還是直接請求伺服器?如下圖網站來做個示例:
第一次開啟該網站後,如果再次重新整理頁面。會發現瀏覽器載入的眾多資源中,有一部分size有具體數值,然而還有一部分請求,比如圖片、css和js等檔案並沒有顯示檔案大小,而是顯示了 from dis cache
或者 from memory cache
字樣。這就說明了,該資源直接從本地硬碟或者瀏覽器記憶體讀取,而並沒有請求伺服器。
瀏覽器啟用快取至少有兩點顯而易見的好處:(1)減少頁面載入時間;(2)減少伺服器負載;
瀏覽器是否使用快取、快取多久,是由伺服器控制的。準確來說,當瀏覽器請求一個網頁(或者其他資源)時,伺服器發回的響應的「響應頭」部分的某些欄位指明瞭有關快取的關鍵資訊。下面看下,HTTP報文中與快取相關的首部欄位:
-
通用首部欄位(就是請求報文和響應報文都能用上的欄位)
-
請求首部欄位
-
響應首部欄位
-
實體首部欄位
3 瀏覽器快取機制
根據上面四種型別的首部欄位不同使用策略,瀏覽器中快取可分為強快取和協商快取:
1)瀏覽器在載入資源時,先根據這個資源的一些http header判斷它是否命中強快取,強快取如果命中,瀏覽器直接從自己的快取中讀取資源,不會發請求到伺服器。比如:某個css檔案,如果瀏覽器在載入它所在的網頁時,這個css檔案的快取配置命中了強快取,瀏覽器就直接從快取中載入這個css,連請求都不會傳送到網頁所在伺服器;
2)當強快取沒有命中的時候,瀏覽器一定會傳送一個請求到伺服器,通過伺服器端依據資源的另外一些http header驗證這個資源是否命中協商快取,如果協商快取命中,伺服器會將這個請求返回,但是不會返回這個資源的資料,而是告訴客戶端可以直接從快取中載入這個資源,於是瀏覽器就又會從自己的快取中去載入這個資源;
3)強快取與協商快取的共同點是:如果命中,都是從客戶端快取中載入資源,而不是從伺服器載入資源資料;區別是:強快取不發請求到伺服器,協商快取會發請求到伺服器。
4)當協商快取也沒有命中的時候,瀏覽器直接從伺服器載入資源資料。
3.1 強快取:Expires&Cache-Control
當瀏覽器對某個資源的請求命中了強快取時,返回的HTTP狀態為200,在chrome的開發者工具的network裡面 size會顯示為from cache,比如:京東的首頁裡就有很多靜態資源配置了強快取,用chrome開啟幾次,再用f12檢視network,可以看到有不少請求就是從快取中載入的:
強快取是利用Expires或者Cache-Control這兩個http response header實現的,它們都用來表示資源在客戶端快取的有效期。
Expires是HTTP 1.0提出的一個表示資源過期時間的header,它描述的是一個絕對時間,由伺服器返回,用GMT格式的字串表示,如:Expires:Thu, 31 Dec 2037 23:55:55 GMT,包含了Expires頭標籤的檔案,就說明瀏覽器對於該檔案快取具有非常大的控制權。
例如,一個檔案的Expires值是2020年的1月1日,那麼就代表,在2020年1月1日之前,瀏覽器都可以直接使用該檔案的本地快取檔案,而不必去伺服器再次請求該檔案,哪怕伺服器檔案發生了變化。
所以,Expires是優化中最理想的情況,因為它根本不會產生請求,所以後端也就無需考慮查詢快慢。它的快取原理,如下:
-
瀏覽器第一次跟伺服器請求一個資源,伺服器在返回這個資源的同時,在response的header加上Expires的header,如:
-
瀏覽器在接收到這個資源後,會把這個資源連同所有response header一起快取下來(所以快取命中的請求返回的header並不是來自伺服器,而是來自之前快取的header);
-
瀏覽器再請求這個資源時,先從快取中尋找,找到這個資源後,拿出它的Expires跟當前的請求時間比較,如果請求時間在Expires指定的時間之前,就能命中快取,否則就不行;
-
如果快取沒有命中,瀏覽器直接從伺服器載入資源時,Expires Header在重新載入的時候會被更新;
Expires是較老的強快取管理header,由於它是伺服器返回的一個絕對時間,在伺服器時間與客戶端時間相差較大時,快取管理容易出現問題,比如:隨意修改下客戶端時間,就能影響快取命中的結果。所以在HTTP 1.1的時候,提出了一個新的header,就是Cache-Control,這是一個相對時間,在配置快取的時候,以秒為單位,用數值表示,如:Cache-Control:max-age=315360000,它的快取原理是:
-
瀏覽器第一次跟伺服器請求一個資源,伺服器在返回這個資源的同時,在response的header加上Cache-Control的header,如:
-
瀏覽器在接收到這個資源後,會把這個資源連同所有response header一起快取下來;
-
瀏覽器再請求這個資源時,先從快取中尋找,找到這個資源後,根據它第一次的請求時間和Cache-Control設定的有效期,計算出一個資源過期時間,再拿這個過期時間跟當前的請求時間比較,如果請求時間在過期時間之前,就能命中快取,否則就不行;
-
如果快取沒有命中,瀏覽器直接從伺服器載入資源時,Cache-Control Header在重新載入的時候會被更新;
Cache-Control描述的是一個相對時間,在進行快取命中的時候,都是利用客戶端時間進行判斷,所以相比較Expires,Cache-Control的快取管理更有效,安全一些。
這兩個header可以只啟用一個,也可以同時啟用,當response header中,Expires和Cache-Control同時存在時,Cache-Control優先順序高於Expires:
此外,還可以為 Cache-Control 指定 public
或 private
標記。如果使用 private,則表示該資源僅僅屬於發出請求的終端使用者,這將禁止中間伺服器(如代理伺服器)快取此類資源。對於包含使用者個人資訊的檔案(如一個包含使用者名稱的 HTML 文件),可以設定 private,一方面由於這些快取對其他使用者來說沒有任何意義,另一方面使用者可能不希望相關檔案儲存在不受信任的伺服器上。需要指出的是,private 並不會使得快取更加安全,它同樣會傳給中間伺服器(如果網站對於傳輸的安全性要求很高,應該使用傳輸層安全措施)。對於 public,則允許所有伺服器快取該資源。通常情況下,對於所有人都可以訪問的資源(例如網站的 logo、圖片、指令碼等),Cache-Control 預設設為 public 是合理的。
3.2 協商快取:Last-Modified&Etag
當瀏覽器對某個資源的請求沒有命中強快取,就會發一個請求到伺服器,驗證協商快取是否命中,如果協商快取命中,請求響應返回的http狀態為304並且會顯示一個Not Modified的字串,比如你開啟京東的首頁,按f12開啟開發者工具,再按f5重新整理頁面,檢視network,可以看到有不少請求就是命中了協商快取的:
檢視單個請求的Response Header,也能看到304的狀態碼和Not Modified的字串,只要看到這個就可說明這個資源是命中了協商快取,然後從客戶端快取中載入的,而不是伺服器最新的資源:
協商快取是利用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】這兩對Header來管理的。
【Last-Modified,If-Modified-Since】的控制快取的原理,如下:
-
瀏覽器第一次跟伺服器請求一個資源,伺服器在返回這個資源的同時,在response的header加上Last-Modified的header,這個header表示這個資源在伺服器上的最後修改時間:
-
瀏覽器再次跟伺服器請求這個資源時,在request的header上加上If-Modified-Since的header,這個header的值就是上一次請求時返回的Last-Modified的值:
-
伺服器再次收到資源請求時,根據瀏覽器傳過來If-Modified-Since和資源在伺服器上的最後修改時間判斷資源是否有變化,如果沒有變化則返回304 Not Modified,但是不會返回資源內容;如果有變化,就正常返回資源內容。當伺服器返回304 Not Modified的響應時,response header中不會再新增Last-Modified的header,因為既然資源沒有變化,那麼Last-Modified也就不會改變,這是伺服器返回304時的response header:
-
瀏覽器收到304的響應後,就會從快取中載入資源。
-
如果協商快取沒有命中,瀏覽器直接從伺服器載入資源時,Last-Modified Header在重新載入的時候會被更新,下次請求時,If-Modified-Since會啟用上次返回的Last-Modified值。
【Last-Modified,If-Modified-Since】都是根據伺服器時間返回的header,一般來說,在沒有調整伺服器時間和篡改客戶端快取的情況下,這兩個header配合起來管理協商快取是非常可靠的,但是有時候也會伺服器上資源其實有變化,但是最後修改時間卻沒有變化的情況,而這種問題又很不容易被定位出來,而當這種情況出現的時候,就會影響協商快取的可靠性。所以就有了另外一對header來管理協商快取,這對header就是【ETag、If-None-Match】。它們的快取管理的方式是:
-
瀏覽器第一次跟伺服器請求一個資源,伺服器在返回這個資源的同時,在response的header加上ETag的header,這個header是伺服器根據當前請求的資源生成的一個唯一標識,這個唯一標識是一個字串,只要資源有變化這個串就不同,跟最後修改時間沒有關係,所以能很好的補充Last-Modified的問題:
-
瀏覽器再次跟伺服器請求這個資源時,在request的header上加上If-None-Match的header,這個header的值就是上一次請求時返回的ETag的值:
-
伺服器再次收到資源請求時,根據瀏覽器傳過來If-None-Match和然後再根據資源生成一個新的ETag,如果這兩個值相同就說明資源沒有變化,否則就是有變化;如果沒有變化則返回304 Not Modified,但是不會返回資源內容;如果有變化,就正常返回資源內容。與Last-Modified不一樣的是,當伺服器返回304 Not Modified的響應時,由於ETag重新生成過,response header中還會把這個ETag返回,即使這個ETag跟之前的沒有變化:
-
瀏覽器收到304的響應後,就會從快取中載入資源。
Etag和Last-Modified非常相似,都是用來判斷一個引數,從而決定是否啟用快取。但是ETag相對於Last-Modified也有其優勢,可以更加準確的判斷檔案內容是否被修改,從而在實際操作中實用程度也更高。
協商快取跟強快取不一樣,強快取不發請求到伺服器,所以有時候資源更新了瀏覽器還不知道,但是協商快取會發請求到伺服器,所以資源是否更新,伺服器肯定知道。大部分web伺服器都預設開啟協商快取,而且是同時啟用【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】,比如apache:
如果沒有協商快取,每個到伺服器的請求,就都得返回資源內容,這樣伺服器的效能會極差。
【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】一般都是同時啟用,這是為了處理Last-Modified不可靠的情況。有一種場景需要注意:
分散式系統裡多臺機器間檔案的Last-Modified必須保持一致,以免負載均衡到不同機器導致比對失敗;
分散式系統儘量關閉掉ETag(每臺機器生成的ETag都會不一樣);
比如,京東頁面的資源請求,返回的repsonse header就只有Last-Modified,沒有ETag:
協商快取需要配合強快取使用,上面這個截圖中,除了Last-Modified這個header,還有強快取的相關header,因為如果不啟用強快取的話,協商快取根本沒有意義。
4 快取判斷流程
如果資源已經被瀏覽器快取下來,在快取失效之前,再次請求時,預設會先檢查是否命中強快取,如果強快取命中則直接讀取快取,如果強快取沒有命中則發請求到伺服器檢查是否命中協商快取,如果協商快取命中,則告訴瀏覽器還是可以從快取讀取,否則才從伺服器返回最新的資源。其瀏覽器判斷快取的詳細流程圖,如下: