者 | 浪里行舟
責編 | 胡巍巍
前言
緩存可以說是性能優化中簡單高效的一種優化方式了。一個優秀的緩存策略可以縮短網頁請求資源的距離,減少延遲,并且由于緩存文件可以重復利用,還可以減少帶寬,降低網絡負荷。
對于一個數據請求來說,可以分為發起網絡請求、后端處理、瀏覽器響應三個步驟。瀏覽器緩存可以幫助我們在第一和第三步驟中優化性能。比如說直接使用緩存而不發起請求,或者發起了請求但后端存儲的數據和前端一致,那么就沒有必要再將數據回傳回來,這樣就減少了響應數據。
接下來的內容中我們將通過緩存位置、緩存策略以及實際場景應用緩存策略來探討瀏覽器緩存機制。
緩存位置
從緩存位置上來說分為四種,并且各自有優先級,當依次查找緩存且都沒有命中的時候,才會去請求網絡。
1.Service Worker
Service Worker 是運行在瀏覽器背后的獨立線程,一般可以用來實現緩存功能。使用 Service Worker的話,傳輸協議必須為 HTTPS。因為 Service Worker 中涉及到請求攔截,所以必須使用 HTTPS 協議來保障安全。Service Worker 的緩存與瀏覽器其他內建的緩存機制不同,它可以讓我們自由控制緩存哪些文件、如何匹配緩存、如何讀取緩存,并且緩存是持續性的。
Service Worker 實現緩存功能一般分為三個步驟:首先需要先注冊 Service Worker,然后監聽到 install 事件以后就可以緩存需要的文件,那么在下次用戶訪問的時候就可以通過攔截請求的方式查詢是否存在緩存,存在緩存的話就可以直接讀取緩存文件,否則就去請求數據。
當 Service Worker 沒有命中緩存的時候,我們需要去調用 fetch 函數獲取數據。也就是說,如果我們沒有在 Service Worker 命中緩存的話,會根據緩存查找優先級去查找數據。但是不管我們是從 Memory Cache 中還是從網絡請求中獲取的數據,瀏覽器都會顯示我們是從 Service Worker 中獲取的內容。
2.Memory Cache
Memory Cache 也就是內存中的緩存,主要包含的是當前中頁面中已經抓取到的資源,例如頁面上已經下載的樣式、腳本、圖片等。讀取內存中的數據肯定比磁盤快,內存緩存雖然讀取高效,可是緩存持續性很短,會隨著進程的釋放而釋放。 一旦我們關閉 Tab 頁面,內存中的緩存也就被釋放了。
那么既然內存緩存這么高效,我們是不是能讓數據都存放在內存中呢?這是不可能的。計算機中的內存一定比硬盤容量小得多,操作系統需要精打細算內存的使用,所以能讓我們使用的內存必然不多。
當我們訪問過頁面以后,再次刷新頁面,可以發現很多數據都來自于內存緩存。
內存緩存中有一塊重要的緩存資源是preloader相關指令(例如 <linkrel="prefetch">)下載的資源??偹苤猵reloader的相關指令已經是頁面優化的常見手段之一,它可以一邊解析js/css文件,一邊網絡請求下一個資源。
需要注意的事情是,內存緩存在緩存資源時并不關心返回資源的HTTP緩存頭Cache-Control是什么值,同時資源的匹配也并非僅僅是對URL做匹配,還可能會對Content-Type,CORS等其他特征做校驗。
3.Disk Cache
Disk Cache 也就是存儲在硬盤中的緩存,讀取速度慢點,但是什么都能存儲到磁盤中,比之 Memory Cache 勝在容量和存儲時效性上。
在所有瀏覽器緩存中,Disk Cache 覆蓋面基本是最大的。它會根據 HTTP Herder 中的字段判斷哪些資源需要緩存,哪些資源可以不請求直接使用,哪些資源已經過期需要重新請求。并且即使在跨站點的情況下,相同地址的資源一旦被硬盤緩存下來,就不會再次去請求數據。絕大部分的緩存都來自 Disk Cache,關于 HTTP 的協議頭中的緩存字段,我們會在下文進行詳細介紹。
瀏覽器會把哪些文件丟進內存中?哪些丟進硬盤中?關于這點,網上說法不一,不過以下觀點比較靠得?。?/p>
4.Push Cache
Push Cache(推送緩存)是 HTTP/2 中的內容,當以上三種緩存都沒有命中時,它才會被使用。它只在會話(Session)中存在,一旦會話結束就被釋放,并且緩存時間也很短暫,在Chrome瀏覽器中只有5分鐘左右,同時它也并非嚴格執行HTTP頭中的緩存指令。
Push Cache 在國內能夠查到的資料很少,也是因為 HTTP/2 在國內不夠普及。這里推薦閱讀 JakeArchibald的 HTTP/2 push is tougher than I thought 這篇文章,文章中的幾個結論:
如果以上四種緩存都沒有命中的話,那么只能發起請求來獲取資源了。
那么為了性能上的考慮,大部分的接口都應該選擇好緩存策略,通常瀏覽器緩存策略分為兩種:強緩存和協商緩存,并且緩存策略都是通過設置 HTTP Header 來實現的。
緩存過程分析
瀏覽器與服務器通信的方式為應答模式,即是:瀏覽器發起HTTP請求 – 服務器響應該請求,那么瀏覽器怎么確定一個資源該不該緩存,如何去緩存呢?瀏覽器第一次向服務器發起該請求后拿到請求結果后,將請求結果和緩存標識存入瀏覽器緩存,瀏覽器對于緩存的處理是根據第一次請求資源時返回的響應頭來確定的。具體過程如下圖:
由上圖我們可以知道:
以上兩點結論就是瀏覽器緩存機制的關鍵,它確保了每個請求的緩存存入與讀取,只要我們再理解瀏覽器緩存的使用規則,那么所有的問題就迎刃而解了,本文也將圍繞著這點進行詳細分析。為了方便大家理解,這里我們根據是否需要向服務器重新發起HTTP請求將緩存過程分為兩個部分,分別是強緩存和協商緩存。
強緩存
強緩存:不會向服務器發送請求,直接從緩存中讀取資源,在chrome控制臺的Network選項中可以看到該請求返回200的狀態碼,并且Size顯示from disk cache或from memory cache。強緩存可以通過設置兩種 HTTP Header 實現:Expires 和 Cache-Control。
1.Expires
緩存過期時間,用來指定資源到期的時間,是服務器端的具體的時間點。也就是說,Expires=max-age + 請求時間,需要和Last-modified結合使用。Expires是Web服務器響應消息頭字段,在響應http請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數據,而無需再次請求。
Expires 是 HTTP/1 的產物,受限于本地時間,如果修改了本地時間,可能會造成緩存失效。
Expires:Wed,22Oct201808:41:00GMT表示資源會在 Wed, 22 Oct 2018 08:41:00 GMT 后過期,需要再次請求。
2.Cache-Control
在HTTP/1.1中,Cache-Control是最重要的規則,主要用于控制網頁緩存。比如當 Cache-Control:max-age=300時,則代表在這個請求正確返回時間(瀏覽器也會記錄下來)的5分鐘內再次加載資源,就會命中強緩存。
Cache-Control 可以在請求頭或者響應頭中設置,并且可以組合使用多種指令:
public:所有內容都將被緩存(客戶端和代理服務器都可緩存)。具體來說響應可被任何中間節點緩存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中間的proxy可以緩存資源,比如下次再請求同一資源proxy1直接把自己緩存的東西給 Browser 而不再向proxy2要。
private:所有內容只有客戶端可以緩存,Cache-Control的默認取值。具體來說,表示中間節點不允許緩存,對于Browser <-- proxy1 <-- proxy2 <-- Server,proxy 會老老實實把Server 返回的數據發送給proxy1,自己不緩存任何數據。當下次Browser再次請求時proxy會做好請求轉發而不是自作主張給自己緩存的數據。
no-cache:客戶端緩存內容,是否使用緩存則需要經過協商緩存來驗證決定。表示不使用 Cache-Control的緩存控制方式做前置驗證,而是使用 Etag 或者Last-Modified字段來控制緩存。需要注意的是,no-cache這個名字有一點誤導。設置了no-cache之后,并不是說瀏覽器就不再緩存數據,只是瀏覽器在使用緩存數據時,需要先確認一下數據是否還跟服務器保持一致。
no-store:所有內容都不會被緩存,即不使用強制緩存,也不使用協商緩存
max-age:max-age=xxx (xxx is numeric)表示緩存內容將在xxx秒后失效
s-maxage(單位為s):同max-age作用一樣,只在代理服務器中生效(比如CDN緩存)。比如當s-maxage=60時,在這60秒中,即使更新了CDN的內容,瀏覽器也不會進行請求。max-age用于普通緩存,而s-maxage用于代理緩存。s-maxage的優先級高于max-age。如果存在s-maxage,則會覆蓋掉max-age和Expires header。
max-stale:能容忍的最大過期時間。max-stale指令標示了客戶端愿意接收一個已經過期了的響應。如果指定了max-stale的值,則最大容忍時間為對應的秒數。如果沒有指定,那么說明瀏覽器愿意接收任何age的響應(age表示響應由源站生成或確認的時間與當前時間的差值)。
min-fresh:能夠容忍的最小新鮮度。min-fresh標示了客戶端不愿意接受新鮮度不多于當前的age加上min-fresh設定的時間之和的響應。
從圖中我們可以看到,我們可以將多個指令配合起來一起使用,達到多個目的。比如說我們希望資源能被緩存下來,并且是客戶端和代理服務器都能緩存,還能設置緩存失效時間等等。
3.Expires和Cache-Control兩者對比
其實這兩者差別不大,區別就在于 Expires 是http1.0的產物,Cache-Control是http1.1的產物,兩者同時存在的話,Cache-Control優先級高于Expires;在某些不支持HTTP1.1的環境下,Expires就會發揮用處。所以Expires其實是過時的產物,現階段它的存在只是一種兼容性的寫法。強緩存判斷是否緩存的依據來自于是否超出某個時間或者某個時間段,而不關心服務器端文件是否已經更新,這可能會導致加載文件不是服務器端最新的內容,那我們如何獲知服務器端內容是否已經發生了更新呢?此時我們需要用到協商緩存策略。
協商緩存
協商緩存就是強制緩存失效后,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程,主要有以下兩種情況:
協商緩存生效,返回304和Not Modified:
協商緩存失效,返回200和請求結果:
協商緩存可以通過設置兩種 HTTP Header 實現:Last-Modified 和 ETag 。
1.Last-Modified和If-Modified-Since
瀏覽器在第一次訪問資源時,服務器返回資源的同時,在response header中添加 Last-Modified的header,值是這個資源在服務器上的最后修改時間,瀏覽器接收后緩存文件和header;
Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT
瀏覽器下一次請求這個資源,瀏覽器檢測到有 Last-Modified這個header,于是添加If-Modified-Since這個header,值就是Last-Modified中的值;服務器再次收到這個資源請求,會根據 If-Modified-Since 中的值與服務器中這個資源的最后修改時間對比,如果沒有變化,返回304和空的響應體,直接從緩存讀取,如果If-Modified-Since的時間小于服務器中這個資源的最后修改時間,說明文件有更新,于是返回新的資源文件和200。
但是 Last-Modified 存在一些弊端:
既然根據文件修改時間來決定是否緩存尚有不足,能否可以直接根據文件內容是否修改來決定緩存策略?所以在 HTTP / 1.1 出現了 ETag 和 If-None-Match
2.ETag和If-None-Match
Etag是服務器響應請求時,返回當前資源文件的一個唯一標識(由服務器生成),只要資源有變化,Etag就會重新生成。瀏覽器在下一次加載資源向服務器發送請求時,會將上一次返回的Etag值放到request header里的If-None-Match里,服務器只需要比較客戶端傳來的If-None-Match跟自己服務器上該資源的ETag是否一致,就能很好地判斷資源相對客戶端而言是否被修改過了。如果服務器發現ETag匹配不上,那么直接以常規GET 200回包形式將新的資源(當然也包括了新的ETag)發給客戶端;如果ETag是一致的,則直接返回304知會客戶端直接使用本地緩存即可。
3.兩者之間對比:
Last-Modified的時間單位是秒,如果某個文件在1秒內改變了多次,那么他們的Last-Modified其實并沒有體現出來修改,但是Etag每次都會改變確保了精度;如果是負載均衡的服務器,各個服務器生成的Last-Modified也有可能不一致。
緩存機制
強制緩存優先于協商緩存進行,若強制緩存(Expires和Cache-Control)生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified / If-Modified-Since和Etag / If-None-Match),協商緩存由服務器決定是否使用緩存,若協商緩存失效,那么代表該請求的緩存失效,返回200,重新返回資源和緩存標識,再存入瀏覽器緩存中;生效則返回304,繼續使用緩存。具體流程圖如下:
看到這里,不知道你是否存在這樣一個疑問:如果什么緩存策略都沒設置,那么瀏覽器會怎么處理?
對于這種情況,瀏覽器會采用一個啟發式的算法,通常會取響應頭中的 Date 減去 Last-Modified 值的 10% 作為緩存時間。
實際場景應用緩存策略
1.頻繁變動的資源
Cache-Control: no-cache
對于頻繁變動的資源,首先需要使用 Cache-Control:no-cache 使瀏覽器每次都請求服務器,然后配合 ETag 或者 Last-Modified 來驗證資源是否有效。這樣的做法雖然不能節省請求數量,但是能顯著減少響應數據大小。
2.不常變化的資源
Cache-Control: max-age=31536000
通常在處理這類資源時,給它們的 Cache-Control 配置一個很大的 max-age=31536000 (一年),這樣瀏覽器之后請求相同的 URL 會命中強制緩存。而為了解決更新的問題,就需要在文件名(或者路徑)中添加 hash, 版本號等動態字符,之后更改動態字符,從而達到更改引用 URL 的目的,讓之前的強制緩存失效 (其實并未立即失效,只是不再使用了而已)。在線提供的類庫 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用這個模式。
用戶行為對瀏覽器緩存的影響
所謂用戶行為對瀏覽器緩存的影響,指的就是用戶在瀏覽器如何操作時,會觸發怎樣的緩存策略。主要有 3 種:
參考文章:
在前端面試中,可能或多或少都會被提及緩存問題,而這個問題大多數都是作為業務中不得不考慮的一個性能優化點,如果平時沒有怎么關注或是特意去了解這塊的童鞋們,可能就是不太了解其中的原由,那么今天我們就這個緩存問題來細細分析,幫助一些還不是太明白的或是剛入門的前端童鞋們梳理梳理,理解理解,那就話不多說,我們開始吧^-^。
其實緩存有很多種,包括:HTTP緩存,DNS緩存,CDN緩存等等。今天主要介紹的就是HTTP緩存,在開始介紹之前,先簡單說說HTTP報文。
HTTP報文就是瀏覽器和服務器之間通信時發送響應的數據塊。
瀏覽器向服務器請求數據,發送請求(request)報文;服務器向瀏覽器返回數據,返回響應(response)報文。
報文主要包含以下兩部分:
1. 屬性的頭部(header);
2. 數據的主體部分(body);
瀏覽器每次發起請求時,先在本地緩存中查找結果以及緩存標識,根據緩存標識來判斷是否使用本地緩存。如果緩存有效,則使用本地緩存;否則,向服務器發起請求并攜帶緩存標識。根據是否需要向服務器發起HTTP請求,將緩存過程劃分為兩個部分:
1. 強制緩存:服務器通知瀏覽器一個緩存時間,在緩存時間內,下次請求直接使用緩存,不在時間內,執行比較緩存策略;
2. 協商緩存:讓客戶端與服務器之間能實現緩存文件是否更新的驗證、提升緩存復用率,將緩存信息中的Etag和Last-Modified通過請求發送給服務器,由服務器校驗,返回304狀態碼時,瀏覽器直接使用緩存;
強緩存優先于協商緩存。
緩存運作的整體流程圖如下:
流程圖
當請求命中強制緩存時,瀏覽器不會將本次請求發往服務器,而是直接從緩存中讀取內容,在Chrome中打開控制臺,在network中顯示的是memory cache或者是disk cache。
緩存
強制緩存可以通過設置兩種HTTP Header實現:Expires(1.0)和Cache-Control(1.1)。
Expires是一個絕對時間,是緩存過期時間。用以表達在這個時間點之前發起請求可以直接從瀏覽器中讀取數據,而無需重新發起請求。
Expires = max-age + 到期時間。由于受限于本地時間,如果修改了本地時間,可能會造成緩存失效。
該字段是服務器響應消息頭字段,告訴瀏覽器在過期時間之前可以直接從瀏覽器緩存中存取數據。由于是絕對時間內,用戶可能將本讀時間進行修改,從而導致瀏覽器判斷緩存失效,重新請求資源。在不考慮修改,時差或者誤差等因素也可能造成客戶端與服務端的時間不一致,致使緩存失效。
優點:
1. HTTP 1.0產物,可以在HTTP 1.0和1.1中使用,簡單、易用。
2. 以時刻標識失效時間。
缺點:
1. 時間是由服務器發送的,如果服務器時間和客戶端時間不一致,可能會出現問題。
2. 存在版本問題,到期之前的修改客戶端是不可知的。
Cache-Control的優先級比Expires的優先級高。該字段表示資源緩存最大有效時間,在該時間內,客戶端不需要向服務器發送請求。前者的出現是為了解決Expires在瀏覽器中,時間被手動更改導致緩存判斷錯誤的問題。如果同時存在則使用Cache-Control。
常見的取值有(完整的列表可以查看MDN):
cache-control
圖中Cache-Control指定了max-age,public,緩存時間為31536000秒(365天)。
也就是說,在365天內再次請求這條數據,都會直接獲取緩存數據庫中的數據,直接使用。
優點:
1. HTTP 1.1產物,以時間間隔標識失效時間,解決了Expires服務器和客戶端相對時間的問題。
2. 比Expires多了很多選項設置。
缺點:
1. 存在版本問題,到期之前的修改客戶端是不可知的。
協商緩存就是強制緩存失效后,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程。而整個過程是需要發出請求的。
協商緩存由2組字段(不是2個),控制協商緩存的字段有:
Etag/If-None-match 的優先級高于Last-Modified/If-Modified-since。
1. 服務器通過 Last-Modified 字段告知客戶端(返回資源的同時在header添加),表示資源最后一次被修改的時間,瀏覽器將這個值和內容一起記錄在緩存數據庫中
2. 下一次請求相同的資源時,瀏覽器會從自己的緩存中找出“不確定是否過期的”緩存,因此在請求頭中將上次的Last-Modified的值寫入到請求頭的If-Modified-since字段
3. 服務器會將If-Modified-since的值與Last-Modified字段進行對比。如果相等,這表示未修改,響應304;反之則表示修改了,響應 200 狀態碼,并返回數據
優點:
1. 不存在版本問題,每次請求都會去服務器進行校驗。服務器對比最后修改時間如果相同則返回304,不同返回200及資源內容。
2. 如果返回的是 304,返回的僅僅是一個狀態碼而已,并沒有實際的文件內容,因此在響應體的體積上節省是很好的優點
缺點:
1. 只要資源發生了修改,無論內容是否發生了實質性的改變,都會將該資源返回客戶端。例如周期性重寫,但這種情況下資源包含的數據實質是一樣的。
2. 以時刻作為標識,無法識別一秒內多次修改的情況。如果資源更新的速度是秒以下的單位,那么該緩存是不能被使用的,因為它的時間最低單位是秒。
3. 某些服務器不能精確的得到文件最后修改時間。
4. 如果文件是服務器動態生成的,那么該方法的更新時間永遠是生成的時間,盡管文件可能并沒有變化,所以也起不到緩存的作用。
為了解決上述問題,出現了一組新的字段Etag/In-None-Match。
1. Etag是上一次加載資源時,服務器返回的。它的作用是唯一用來標識資源是否有變化
2. 瀏覽器在下一次發起請求時,會將上一次返回的Etag值賦值給If-None-Match并添加在 Request Header 中。服務端匹配傳入的值與上次是否一致,如果一致返回304,瀏覽器則讀取本地緩存,否則返回200和更新后的資源及新的Etag
優點:
1. 可以更加精確的判斷資源是否被修改,可以識別一秒內多次修改的情況
2. 不存在版本問題,每次請求都會去服務器進行校驗
缺點:
1. 計算Etag值需要性能損耗
2. 分布式服務器存儲情況下下,計算Etag的算法如果不一致,會導致瀏覽器從一個服務器上獲取得頁面內容后到另一臺服務器上進行驗證時出現Etag不匹配的情況
對于強制緩存,服務器通知瀏覽器一個緩存時間,在緩存時間內,下次請求,直接用緩存,不在時間內,執行比較緩存策略。
對于協商緩存,將緩存信息中的Etag和Last-Modified通過請求發送給服務器,由服務器校驗,返回304狀態碼時,瀏覽器直接使用緩存。
文分享自華為云社區《面試官怒了:多級緩存不了解怎么行,那可是數量級的提升?》,作者: 知識淺談 。
從客戶端到服務端,多級緩存層層剖析總結
主要是對html種css,js,圖片等一些靜態資源進行緩存到本地,在之后進行網頁訪問的時候后就不用再次去服務端去獲取了。
常用的像是百度的logo標志是設置expires這個過期時間來限制這個logo靜態資源的多次訪問。
CDN內容分發網絡/Nginx負載均衡層是主要通過在CDN的節點或者是Nginx的節點上進行緩存來達到內容的緩存便于快速的查找資源。
CDN層緩存
Nginx層緩存:
作為進程內部緩存的意思是在運行的過程中,應用數據被載入到進程中的,通過本地內存的低延遲高吞吐的特性來調高數據資源的查詢效率。
像是Mybatis的一二級緩存,Hibernate,Springmvc的頁面緩存,對于這些緩存的實現有Ehcache等。
針對分布式的架構中,我可以使用Redis這種Nosql數據庫對整體架構中應用數據進行緩存,一般想起緩存我們都是使用Redis來解決其實,在高并發的環境中所有的請求都打到Redis這種nosql緩存上也不是一個好的選擇,所以還像是靜態資源還需要客戶端緩存和Encache這種進程中的緩存來一起實現。
針對從客戶端到服務的不同層面做了一個總體不同的緩存實現形式的總結,希望對你有所幫助。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。