前述
對一個網站而言,CSS、javascript、logo、圖標這些靜態資源文件更新的頻率都比較低,而這些文件又幾乎是每次http請求都需要的,如果將這些文件緩存在瀏覽器中,可以極好的改善性能。
緩存可以說是性能優化中簡單高效的一種優化方式了。一個優秀的緩存策略可以縮短網頁請求資源的距離,減少延遲,並且由於緩存文件可以重復利用,還可以減少帶寬,降低網絡負荷。
對於一個數據請求來說,可以分為發起網絡請求、后端處理、瀏覽器響應三個步驟。瀏覽器緩存可以幫助我們在第一和第三步驟中優化性能。比如說直接使用緩存而不發起請求,或者發起了請求但后端存儲的數據和前端一致,那么就沒有必要再將數據回傳回來,這樣就減少了響應數據。
通過設置http頭中的cache-control和expires的屬性和HTML Meta標簽,可設定瀏覽器緩存,緩存時間可以是數天,甚至是幾個月。
瀏覽器緩存控制機制有兩種:HTML Meta標簽 vs. HTTP頭信息(主要)
緩存位置
從緩存位置上來說分為四種,並且各自有優先級,當依次查找緩存且都沒有命中的時候,才會去請求網絡。
緩存位置——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 中獲取的內容。
緩存位置——Memory Cache
Memory Cache 也就是內存中的緩存,主要包含的是當前中頁面中已經抓取到的資源,例如頁面上已經下載的樣式、腳本、圖片等。讀取內存中的數據肯定比磁盤快,內存緩存雖然讀取高效,可是緩存持續性很短,會隨着進程的釋放而釋放。 一旦我們關閉 Tab 頁面,內存中的緩存也就被釋放了。
那么既然內存緩存這么高效,我們是不是能讓數據都存放在內存中呢?
這是不可能的。計算機中的內存一定比硬盤容量小得多,操作系統需要精打細算內存的使用,所以能讓我們使用的內存必然不多。
當我們訪問過頁面以后,再次刷新頁面,可以發現很多數據都來自於內存緩存

緩存過程分析
瀏覽器與服務器通信的方式為應答模式,即是:瀏覽器發起HTTP請求 – 服務器響應該請求,
那么瀏覽器怎么確定一個資源該不該緩存,如何去緩存呢?瀏覽器第一次向服務器發起該請求后拿到請求結果后,將請求結果和緩存標識存入瀏覽器緩存,
瀏覽器對於緩存的處理是根據第一次請求資源時返回的響應頭來確定的。具體過程如下圖:
瀏覽器每次發起請求,都會先在瀏覽器緩存中查找該請求的結果以及緩存標識
瀏覽器每次拿到返回的請求結果都會將該結果和緩存標識存入瀏覽器緩存中
HTML Meta 標簽定義的緩存機制
可以在HTML頁面的<head>節點中加入<meta>標簽,這樣寫的話僅對該網頁有效,對網頁中的圖片或其他請求無效。代碼如下:
<meta http-equiv="Cache-Control" content="max-age=7200" /> <meta http-equiv="Expires" content="Sun Oct 15 2017 20:39:53 GMT+0800 (CST)" />
Pragma是http1.0版本中給客戶端設定緩存方式之一
下面代碼的作用是告訴瀏覽器當前頁面不被緩存,每次訪問都需要去服務器拉取。使用上很簡單,但只有部分瀏覽器可以支持,而且所有緩存代理服務器都不支持,因為代理不解析HTML內容本身。
僅有IE才能識別這段meta標簽含義,其它主流瀏覽器僅識別“Cache-Control: no-store”的meta標簽。
在IE中識別到該meta標簽含義,並不一定會在請求字段加上Pragma,但的確會讓當前頁面每次都發新請求(僅限頁面,頁面上的資源則不受影響)。
HTTP頭信息緩存機制:
HTTP頭信息緩存機制有可以分為兩種強緩存(完全不問服務端資源有沒有更新,直接拿瀏覽器緩存)和協商緩存
強緩存
不會向服務器發送請求,直接從緩存中讀取資源,在chrome控制台的Network選項中可以看到該請求返回200的狀態碼,並且Size顯示from disk cache或from memory cache。
Expires策略
Expires是Web服務器響應消息頭字段,在響應http請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數據,而無需再次請求。
不過Expires 是HTTP 1.0的東西,現在默認瀏覽器均默認使用HTTP 1.1,所以它的作用基本忽略。
Expires 的一個缺點就是,返回的到期時間是服務器端的時間,這樣存在一個問題,如果客戶端的時間與服務器的時間相差很大(比如時鍾不同步,或者跨時區),那么誤差就很大,所以在HTTP 1.1版開始,使用Cache-Control: max-age=秒替代。
Cache-control策略(重點關注)
Cache-Control與Expires的作用一致,都是指明當前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數據還是重新發請求到服務器取數據。
在HTTP/1.1中,Cache-Control是最重要的規則,主要用於控制網頁緩存。比如當Cache-Control:max-age=300
時,則代表在這個請求正確返回時間(瀏覽器也會記錄下來)的5分鍾內再次加載資源,就會命中強緩存。
只不過Cache-Control的選擇更多,設置更細致,如果同時設置的話,其優先級高於Expires。
並且可以組合使用多種指令:值可以是public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-ageCache-Control 。如((Cache-Control:private, s-maxage=0))
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設定的時間之和的響應。
我們可以將多個指令配合起來一起使用,達到多個目的。比如說我們希望資源能被緩存下來,並且是客戶端和代理服務器都能緩存,還能設置緩存失效時間等等。
代碼演示,首先我們創建一個node服務,並且寫一個請求文件的的request
const http = require('http') const fs = require('fs') http.createServer(function (request, response) { console.log('request come', request.url) if (request.url === '/') { const html = fs.readFileSync('test.html', 'utf8') response.writeHead(200, { 'Content-Type': 'text/html' }) response.end(html) } if (request.url === '/script.js') { response.writeHead(200, { 'Content-Type': 'text/javascript', 'Cache-Control': 'max-age=200' // 'Cache-Control': 'max-age=200, public' //可以設置多個,號分隔 }) response.end('console.log("script loaded")') } }).listen(8888) console.log('server listening on 8888')
在test.html中發送一個請求script.js的文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> </body> <script src="/script.js"></script> </html>
啟動服務,並且訪問test.html,發送請求,運行結果可以看到第一次是實實在在的發起了請求的,再次刷新的時候,就直接從緩存中拿了,沒有再發送請求
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 。
Last-Modified/If-Modified-Since
瀏覽器在第一次訪問資源時,服務器返回資源的同時,在response header中添加 Last-Modified的header,值是這個資源在服務器上的最后修改時間,瀏覽器接收后緩存文件和header;
Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT
瀏覽器下一次請求這個資源,當資源過期時(使用Cache-Control標識的max-age),瀏覽器檢測到有 Last-Modified這個header,於是添加If-Modified-Since這個header,值就是Last-Modified中的值;服務器再次收到這個資源請求,會根據 If-Modified-Since 中的值與服務器中這個資源的最后修改時間對比,如果沒有變化,返回304和空的響應體,直接從緩存讀取,如果If-Modified-Since的時間小於服務器中這個資源的最后修改時間,說明文件有更新,於是返回新的資源文件和200

如果本地打開緩存文件,即使沒有對文件進行修改,但還是會造成 Last-Modified 被修改,服務端不能命中緩存導致發送相同的資源
因為 Last-Modified 只能以秒計時,如果在不可感知的時間內修改完成文件,那么服務端會認為資源還是命中了,不會返回正確的資源
既然根據文件修改時間來決定是否緩存尚有不足,能否可以直接根據文件內容是否修改來決定緩存策略?所以在 HTTP / 1.1 出現了 ETag
和If-None-Match
ETag和If-None-Match
Etag是服務器響應請求時,返回當前資源文件的一個唯一標識(由服務器生成),只要資源有變化,Etag就會重新生成。
瀏覽器在下一次加載資源向服務器發送請求時,當資源過期時(使用Cache-Control標識的max-age),發現資源具有Etag聲明,會將上一次返回的Etag值放到request header里的If-None-Match里,服務器只需要比較客戶端傳來的If-None-Match跟自己服務器上該資源的ETag是否一致,就能很好地判斷資源相對客戶端而言是否被修改過了。如果服務器發現ETag匹配不上,那么直接以常規GET 200回包形式將新的資源(當然也包括了新的ETag)發給客戶端;如果ETag是一致的,則直接返回304知會客戶端直接使用本地緩存即可。
Last-Modified/Etag兩者之間對比
Last-Modified/If-Modified-Since要配合Cache-Control使用。
Etag/If-None-Match:Etag/If-None-Match也要配合Cache-Control使用。
首先在精確度上,Etag要優於Last-Modified,Last-Modified與ETag一起使用時,服務器會優先驗證ETag。
Last-Modified的時間單位是秒,如果某個文件在1秒內改變了多次,那么他們的Last-Modified其實並沒有體現出來修改,但是Etag每次都會改變確保了精度;如果是負載均衡的服務器,各個服務器生成的Last-Modified也有可能不一致。
第二在性能上,Etag要遜於Last-Modified,畢竟Last-Modified只需要記錄時間,而Etag需要服務器通過算法來計算出一個hash值。
緩存機制
強制緩存優先於協商緩存進行,若強制緩存(Expires和Cache-Control)生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified / If-Modified-Since和Etag / If-None-Match),
如果什么緩存策略都沒設置,那么瀏覽器會怎么處理?
對於這種情況,瀏覽器會采用一個啟發式的算法,通常會取響應頭中的 Date 減去 Last-Modified 值的 10% 作為緩存時間。
實際場景應用緩存策略
頻繁變動的資源
對於頻繁變動的資源,首先需要使用Cache-Control: no-cache
使瀏覽器每次都請求服務器,然后配合 ETag 或者 Last-Modified 來驗證資源是否有效。這樣的做法雖然不能節省請求數量,但是能顯著減少響應數據大小。
不常變化的資源
max-age=31536000
(一年),這樣瀏覽器之后請求相同的 URL 會命中強制緩存。而為了解決更新的問題,就需要在文件名(或者路徑)中添加 hash, 版本號等動態字符,之后更改動態字符,從而達到更改引用 URL 的目的,讓之前的強制緩存失效 (其實並未立即失效,只是不再使用了而已)。
在線提供的類庫 (如
jquery-3.3.1.min.js
,
lodash.min.js
等) 均采用這個模式。
用戶行為對瀏覽器緩存的影響
瀏覽器緩存行為還有用戶的行為有關,所謂用戶行為對瀏覽器緩存的影響,指的就是用戶在瀏覽器如何操作時,會觸發怎樣的緩存策略。主要有 3 種:
打開網頁,地址欄輸入地址: 查找 disk cache 中是否有匹配。如有則使用;如沒有則發送網絡請求。
普通刷新 (F5):因為 TAB 並沒有關閉,因此 memory cache 是可用的,會被優先使用(如果匹配的話)。其次才是 disk cache。
強制刷新 (Ctrl + F5):瀏覽器不使用緩存,因此發送的請求頭部均帶有 Cache-control: no-cache
(為了兼容,還帶了 Pragma: no-cache
),服務器直接返回 200 和最新內容。