總結
1、瀏覽器第一次發起一個http/https請求,讀取伺服器的資源
2、服務端設定響應頭(cache-control、Expires、last-modified、Etag)給瀏覽器
2.1. cache-control、Expires 屬於強快取,last-modified、Etag屬於對比快取(協商快取)
3、瀏覽器不關閉tab、f5重新整理頁面(再次發起一個請求給伺服器)
3.1、如果cache-control的max-age 和 Expires 未超過快取時間,所有資源除了index.html 都來自於記憶體快取(from memory cache)載入。且狀態碼為200
3.2、如果cache-control的max-age快取時間為5s, Expires的過期時間是超過5s,則cache-control會覆蓋Expires
3.3、如果強快取失效,則下一步會走對比快取。瀏覽器會從第二步的拿到的響應頭,在重新整理發起請求會設定 a、if-modified-since值為響應的last-modified的值; b、if-none-match 值為響應的Etag的值;
3.4、如果if-modified-since 和if-none-match都存在,則if-none-match的優先比if-modified-since高。直接對比第二步給瀏覽器的Etag的值,如果相等就直接返回一個狀態為304不返回內容,如果不相等就返回一個狀態碼為200,並且會返回內容和cache-control 、Expires、last-modified、Etag等響應頭;
3.5、如果if-modified-since 存在, if-none-match不存在,步驟跟上述的3.4類似,只不過服務端對比的是if-modified-since 和第一次返回給瀏覽器last-modified的值
4、如果瀏覽器關閉tab。重新開啟新tab,發起請求資源。步驟跟上述3類似,只不過在上述3.1中,左右資源除了index.html快取(from disk cache)都從磁碟載入。
http快取分為強快取 和 對比快取(協商快取)
1、強快取:
當客戶端請求後,會先訪問快取資料庫看快取是否存在。如果存在則直接返回,不存在則請求真的伺服器。
強制快取直接減少請求數,是提升最大的快取策略。 它的優化覆蓋了文章開頭提到過的請求資料的全部三個步驟。如果考慮使用快取來優化網頁效能的話,強制快取應該是首先被考慮的。
可以造成強制快取的欄位是 Cache-control 和 Expires
Expires
這是 HTTP 1.0 的欄位,表示快取到期時間,是一個絕對的時間 (當前時間+快取時間)。在響應訊息頭中,設定這個欄位之後,就可以告訴瀏覽器,在未過期之前不需要再次請求。
Expires: Thu, 22 Mar 2029 16:06:42 GMT
const http = require('http')
const url = require('url')
const path = require('path')
const fs = require('fs')
http.createServer((req, res) => {
let { pathname } = url.parse(req.url, true);
console.log(pathname)
let abs = path.join(__dirname, pathname);
res.setHeader('Expires', new Date(Date.now() + 20000).toGMTString());
fs.stat(path.join(__dirname, pathname), (err, stat) => {
if(err) {
res.statusCode = 404;
res.end('not found')
return
}
if(stat.isFile()) {
fs.createReadStream(abs).pipe(res)
}
})
}).listen(3000)
複製程式碼
以上程式碼給Expires設定過期時間為20s。
-
首次請求 首次請求 全部走網路請求
-
20s內F5重新整理當前,從記憶體裡面載入。因為我們沒有關閉TAB,所以瀏覽器把快取的應用加到了記憶體快取。(耗時0ms,也就是1ms以內)
-
20s內關閉tab,開啟請求的url,從磁碟載入
關閉了TAB,記憶體快取也隨之清空。但是磁碟快取是持久的,於是所有資源來自磁碟快取。(大約耗時3ms,因為檔案有點小)而且對比2和3,很明顯看到記憶體快取還是比disk cache快得多的。 -
20s以後請求,快取已經失效,重複第1步
過期的缺點:
在這裡,其他電腦訪問伺服器,若修改電腦的本地時間,會導致瀏覽器判斷快取失效
這裡修重新修改快取時間:
res.setHeader('Expires',new Date(Date.now()+ 2000000).toGMTString())
Cache-control
已知Expires的缺點之後,在HTTP/1.1中,增加了一個欄位Cache-control,該欄位表示資源快取的最大有效時間,在該時間內,客戶端不需要向伺服器傳送請求
Expires 和 Cache-control 區別
Expires設定的是 絕對時間
Cache-control設定的是 相對時間
快取控制的優先順序大於到期
複製程式碼
Cache-control: max-age=20
- Cache-control:max-age = 20 max-age最大有效時間
const http = require('http')
const url = require('url')
const path = require('path')
const fs = require('fs')
http.createServer((req, res) => {
let { pathname } = url.parse(req.url, true);
console.log(pathname)
let abs = path.join(__dirname, pathname);
res.setHeader('Cache-Control', 'max-age=20')
fs.stat(path.join(__dirname, pathname), (err, stat) => {
if(err) {
res.statusCode = 404;
res.end('not found')
return
}
if(stat.isFile()) {
fs.createReadStream(abs).pipe(res)
}
})
}).listen(3000)
複製程式碼
以上程式碼給cache-control設定max-age為20s
解析:首次請求->關閉tab再次請求參考Expires的圖
- no-cache 告訴瀏覽器忽略資源的快取副本,強制每次請求直接傳送給伺服器,拉取資源,但不是“不快取”
- no-store 強制快取在任何情況下都不要保留任何副本
- public 任何路徑的快取者(本地快取、代理伺服器),可以無條件的快取改資源
- private 只針對單個使用者或者實體(不同使用者、視窗)快取資源
no-store 和 no-cache的區別
-
no-store: 如果伺服器再響應中設定了no-store。那麼瀏覽器不會儲存這次相應的資料,當下次請求時,瀏覽器會在請求一次,就是說不會對比Etag
res.setHeader('Cache-control', 'no-store')
-
no-cache 如果伺服器在響應中設定了no-cache,那麼說明瀏覽器在使用快取前會對比Etag,返回304就會避免修改
public 和 private
- 設定了public,表示該響應可以在使用者的瀏覽器或者任何中繼web代理對其進行快取,不寫預設為public,表示只有使用者的瀏覽器可以快取private響應不允許任何web代理進行快取, 只有使用者的瀏覽器可以進行快取。
2、對比快取(協商快取)
當強制快取失效(超過規定時間)時,就需要使用對比快取,由伺服器決定快取內容是否失效。對比快取是可以和強制快取一起使用。
last-modified
1、伺服器在響應頭中設定last-modified欄位返回給客戶端,告訴客戶端資源最後一次修改的時間。
Last-Modified: Sat, 30 Mar 2019 05:46:11 GMT
2、瀏覽器在這個值和內容記錄在瀏覽器的快取資料庫中。
3、下次請求相同資源,瀏覽器將在請求頭中設定if-modified-since的值(這個值就是第一步響應頭中的Last-Modified的值)傳給伺服器
4、伺服器收到請求頭的if-modified-since的值與last-modified的值比較,如果相等,表示未進行修改,則返回狀態碼為304;如果不相等,則修改了,返回狀態碼為200,並返回資料
http.createServer((req, res) => {
let { pathname } = url.parse(req.url, true);
console.log(pathname);
let abs = path.join(__dirname, pathname);
fs.stat(path.join(__dirname, pathname), (err, stat) => {
if(err) {
res.statusCode = 404;
res.end('Not Fount');
return
}
if(stat.isFile()) {
res.setHeader('Last-Modified', stat.ctime.toGMTString())
console.log(stat.ctime.toGMTString())
if(req.headers['if-modified-since'] === stat.ctime.toGMTString()) {
console.log('if-modifined-since', req.headers['if-modified-since'])
res.statusCode = 304;
res.end()
return
}
fs.createReadStream(abs).pipe(res)
}
})
}).listen(3000)
複製程式碼
last-modified的缺點:
- last-modified是以秒為單位的,假如資料在1s內可能修改幾次,那麼該快取就不能被使用的。
- 如果檔案是通過伺服器動態生成,那麼更新的時間永遠就是生成的時間,儘管檔案可能沒有變化,所以起不到快取的作用。
Etag
為了解決上述問題,出現了一組新的欄位 Etag 和 If-None-Match
Etag是根絕檔案內容,算出一個唯一的值。伺服器儲存著檔案的 Etag 欄位。之後的流程和 Last-Modified 一致,只是 Last-Modified 欄位和它所表示的更新時間改變成了 Etag 欄位和它所表示的檔案 hash,把 If-Modified-Since 變成了 If-None-Match。伺服器同樣進行比較,命中返回 304, 不命中返回新資源和 200。 Etag 的優先順序高於 Last-Modified
http.createServer(function(req, res) {
let { pathname } = url.parse(req.url, true);
console.log(pathname)
let abs = path.join(__dirname, pathname);
fs.stat(path.join(__dirname, pathname), (err, stat) => {
if(err) {
res.statusCode = 404;
res.end('Not Found')
return
}
if(stat.isFile()) {
//Etag 實體內容,他是根絕檔案內容,算出一個唯一的值。
let md5 = crypto.createHash('md5')
let rs = fs.createReadStream(abs)
let arr = []; // 你要先寫入響應頭再寫入響應體
rs.on('data', function(chunk) {
md5.update(chunk);
arr.push(chunk)
})
rs.on('end', function() {
let etag = md5.digest('base64');
if(req.headers['if-none-match'] === etag) {
console.log(req.headers['if-none-match'])
res.statusCode = 304;
res.end()
return
}
res.setHeader('Etag', etag)
// If-None-Match 和 Etag 是一對, If-None-Match是瀏覽器的, Etag是服務端的
res.end(Buffer.concat(arr))
})
}
})
}).listen(3000)
複製程式碼
Etag的缺點:
- 每次請求的時候,伺服器都會把index.html 讀取一次,以確認檔案有沒有修改
- 2.對大檔案進行etag 一般用檔案的大小 + 檔案的最後修改時間 來組合生成這個etag