實(shí)踐這一次,徹底搞懂瀏覽器緩存機(jī)制

前言

[實(shí)踐系列] 主要是讓我們通過(guò)實(shí)踐去加深對(duì)一些原理的理解。

[實(shí)踐系列]前端路由

[實(shí)踐系列]Babel原理

[實(shí)踐系列]Promises/A+規(guī)范

有興趣的同學(xué)可以關(guān)注 [實(shí)踐系列] 。 求star求follow~

如果覺(jué)得自己已經(jīng)掌握瀏覽器緩存機(jī)制知識(shí)的同學(xué),可以直接看實(shí)踐部分哈~

目錄

 1. DNS 緩存   // 雖說(shuō)跟標(biāo)題關(guān)系不大,了解一下也不錯(cuò)
 2. CDN 緩存   // 雖說(shuō)跟標(biāo)題關(guān)系不大,了解一下也不錯(cuò)
 3. 瀏覽器緩存 // 本文將重點(diǎn)介紹并實(shí)踐  

DNS 緩存

什么是DNS

全稱(chēng) Domain Name System ,即域名系統(tǒng)。

萬(wàn)維網(wǎng)上作為域名和IP地址相互映射的一個(gè)分布式數(shù)據(jù)庫(kù),能夠使用戶(hù)更方便的訪問(wèn)互聯(lián)網(wǎng),而不用去記住能夠被機(jī)器直接讀取的IP數(shù)串。DNS協(xié)議運(yùn)行在UDP協(xié)議之上,使用端口號(hào)53。

DNS解析

簡(jiǎn)單的說(shuō),通過(guò)域名,最終得到該域名對(duì)應(yīng)的IP地址的過(guò)程叫做域名解析(或主機(jī)名解析)。

www.dnscache.com (域名)  - DNS解析 -> 11.222.33.444 (IP地址)

DNS緩存

有dns的地方,就有緩存。瀏覽器、操作系統(tǒng)、Local DNS、根域名服務(wù)器,它們都會(huì)對(duì)DNS結(jié)果做一定程度的緩存。

DNS查詢(xún)過(guò)程如下:

  1. 首先搜索瀏覽器自身的DNS緩存,如果存在,則域名解析到此完成。

  2. 如果瀏覽器自身的緩存里面沒(méi)有找到對(duì)應(yīng)的條目,那么會(huì)嘗試讀取操作系統(tǒng)的hosts文件看是否存在對(duì)應(yīng)的映射關(guān)系,如果存在,則域名解析到此完成。

  1. 如果本地hosts文件不存在映射關(guān)系,則查找本地DNS服務(wù)器(ISP服務(wù)器,或者自己手動(dòng)設(shè)置的DNS服務(wù)器),如果存在,域名到此解析完成。

  2. 如果本地DNS服務(wù)器還沒(méi)找到的話,它就會(huì)向根服務(wù)器發(fā)出請(qǐng)求,進(jìn)行遞歸查詢(xún)。

戳此處詳細(xì)了解DNS解析過(guò)程

CDN 緩存

什么是CDN

全稱(chēng) Content Delivery Network,即內(nèi)容分發(fā)網(wǎng)絡(luò)。

摘錄一個(gè)形象的比喻,來(lái)理解CDN是什么。

10年前,還沒(méi)有火車(chē)票代售點(diǎn)一說(shuō),12306.cn更是無(wú)從說(shuō)起。那時(shí)候火車(chē)票還只能在火車(chē)站的售票大廳購(gòu)買(mǎi),而我所在的小縣城并不通火車(chē),火車(chē)票都要去市里的火車(chē)站購(gòu)買(mǎi),而從我家到縣城再到市里,來(lái)回就是4個(gè)小時(shí)車(chē)程,簡(jiǎn)直就是浪費(fèi)生命。后來(lái)就好了,小縣城里出現(xiàn)了火車(chē)票代售點(diǎn),甚至鄉(xiāng)鎮(zhèn)上也有了代售點(diǎn),可以直接在代售點(diǎn)購(gòu)買(mǎi)火車(chē)票,方便了不少,全市人民再也不用在一個(gè)點(diǎn)苦逼的排隊(duì)買(mǎi)票了。

簡(jiǎn)單的理解CDN就是這些代售點(diǎn)(緩存服務(wù)器)的承包商,他為買(mǎi)票者提供了便利,幫助他們?cè)谧罱牡胤?最近的CDN節(jié)點(diǎn))用最短的時(shí)間(最短的請(qǐng)求時(shí)間)買(mǎi)到票(拿到資源),這樣去火車(chē)站售票大廳排隊(duì)的人也就少了。也就減輕了售票大廳的壓力(起到分流作用,減輕服務(wù)器負(fù)載壓力)。

用戶(hù)在瀏覽網(wǎng)站的時(shí)候,CDN會(huì)選擇一個(gè)離用戶(hù)最近的CDN邊緣節(jié)點(diǎn)來(lái)響應(yīng)用戶(hù)的請(qǐng)求,這樣海南移動(dòng)用戶(hù)的請(qǐng)求就不會(huì)千里迢迢跑到北京電信機(jī)房的服務(wù)器(假設(shè)源站部署在北京電信機(jī)房)上了。

CDN緩存

關(guān)于CDN緩存,在瀏覽器本地緩存失效后,瀏覽器會(huì)向CDN邊緣節(jié)點(diǎn)發(fā)起請(qǐng)求。類(lèi)似瀏覽器緩存,CDN邊緣節(jié)點(diǎn)也存在著一套緩存機(jī)制。CDN邊緣節(jié)點(diǎn)緩存策略因服務(wù)商不同而不同,但一般都會(huì)遵循h(huán)ttp標(biāo)準(zhǔn)協(xié)議,通過(guò)http響應(yīng)頭中的

Cache-control: max-age   //后面會(huì)提到

的字段來(lái)設(shè)置CDN邊緣節(jié)點(diǎn)數(shù)據(jù)緩存時(shí)間。

當(dāng)瀏覽器向CDN節(jié)點(diǎn)請(qǐng)求數(shù)據(jù)時(shí),CDN節(jié)點(diǎn)會(huì)判斷緩存數(shù)據(jù)是否過(guò)期,若緩存數(shù)據(jù)并沒(méi)有過(guò)期,則直接將緩存數(shù)據(jù)返回給客戶(hù)端;否則,CDN節(jié)點(diǎn)就會(huì)向服務(wù)器發(fā)出回源請(qǐng)求,從服務(wù)器拉取最新數(shù)據(jù),更新本地緩存,并將最新數(shù)據(jù)返回給客戶(hù)端。 CDN服務(wù)商一般會(huì)提供基于文件后綴、目錄多個(gè)維度來(lái)指定CDN緩存時(shí)間,為用戶(hù)提供更精細(xì)化的緩存管理。

CDN 優(yōu)勢(shì)

  1. CDN節(jié)點(diǎn)解決了跨運(yùn)營(yíng)商和跨地域訪問(wèn)的問(wèn)題,訪問(wèn)延時(shí)大大降低。
  2. 大部分請(qǐng)求在CDN邊緣節(jié)點(diǎn)完成,CDN起到了分流作用,減輕了源服務(wù)器的負(fù)載。

戳此處詳細(xì)了解CDN工作過(guò)程

瀏覽器緩存(http緩存)

對(duì)著這張圖先發(fā)呆30秒~

image

什么是瀏覽器緩存

image

簡(jiǎn)單來(lái)說(shuō),瀏覽器緩存其實(shí)就是瀏覽器保存通過(guò)HTTP獲取的所有資源,是瀏覽器將網(wǎng)絡(luò)資源存儲(chǔ)在本地的一種行為。

緩存的資源去哪里了?

你可能會(huì)有疑問(wèn),瀏覽器存儲(chǔ)了資源,那它把資源存儲(chǔ)在哪里呢?

memory cache

MemoryCache顧名思義,就是將資源緩存到內(nèi)存中,等待下次訪問(wèn)時(shí)不需要重新下載資源,而直接從內(nèi)存中獲取。Webkit早已支持memoryCache。
目前Webkit資源分成兩類(lèi),一類(lèi)是主資源,比如HTML頁(yè)面,或者下載項(xiàng),一類(lèi)是派生資源,比如HTML頁(yè)面中內(nèi)嵌的圖片或者腳本鏈接,分別對(duì)應(yīng)代碼中兩個(gè)類(lèi):MainResourceLoader和SubresourceLoader。雖然Webkit支持memoryCache,但是也只是針對(duì)派生資源,它對(duì)應(yīng)的類(lèi)為CachedResource,用于保存原始數(shù)據(jù)(比如CSS,JS等),以及解碼過(guò)的圖片數(shù)據(jù)。

disk cache

DiskCache顧名思義,就是將資源緩存到磁盤(pán)中,等待下次訪問(wèn)時(shí)不需要重新下載資源,而直接從磁盤(pán)中獲取,它的直接操作對(duì)象為CurlCacheManager。

  • |memory cache | disk cache
    :---:|:---:|:---:
    相同點(diǎn) |只能存儲(chǔ)一些派生類(lèi)資源文件 | 只能存儲(chǔ)一些派生類(lèi)資源文件
    不同點(diǎn) |退出進(jìn)程時(shí)數(shù)據(jù)會(huì)被清除 | 退出進(jìn)程時(shí)數(shù)據(jù)不會(huì)被清除
    存儲(chǔ)資源|一般腳本、字體、圖片會(huì)存在內(nèi)存當(dāng)中|一般非腳本會(huì)存在內(nèi)存當(dāng)中,如css等

因?yàn)镃SS文件加載一次就可渲染出來(lái),我們不會(huì)頻繁讀取它,所以它不適合緩存到內(nèi)存中,但是js之類(lèi)的腳本卻隨時(shí)可能會(huì)執(zhí)行,如果腳本在磁盤(pán)當(dāng)中,我們?cè)趫?zhí)行腳本的時(shí)候需要從磁盤(pán)取到內(nèi)存中來(lái),這樣IO開(kāi)銷(xiāo)就很大了,有可能導(dǎo)致瀏覽器失去響應(yīng)。

三級(jí)緩存原理 (訪問(wèn)緩存優(yōu)先級(jí))

  1. 先在內(nèi)存中查找,如果有,直接加載。
  2. 如果內(nèi)存中不存在,則在硬盤(pán)中查找,如果有直接加載。
  3. 如果硬盤(pán)中也沒(méi)有,那么就進(jìn)行網(wǎng)絡(luò)請(qǐng)求。
  4. 請(qǐng)求獲取的資源緩存到硬盤(pán)和內(nèi)存。

瀏覽器緩存的分類(lèi)

  1. 強(qiáng)緩存

  2. 協(xié)商緩存

瀏覽器再向服務(wù)器請(qǐng)求資源時(shí),首先判斷是否命中強(qiáng)緩存,再判斷是否命中協(xié)商緩存!

瀏覽器緩存的優(yōu)點(diǎn)

1.減少了冗余的數(shù)據(jù)傳輸

2.減少了服務(wù)器的負(fù)擔(dān),大大提升了網(wǎng)站的性能

3.加快了客戶(hù)端加載網(wǎng)頁(yè)的速度

強(qiáng)緩存

瀏覽器在加載資源時(shí),會(huì)先根據(jù)本地緩存資源的 header 中的信息判斷是否命中強(qiáng)緩存,如果命中則直接使用緩存中的資源不會(huì)再向服務(wù)器發(fā)送請(qǐng)求。

這里的 header 中的信息指的是 expires 和 cahe-control.

Expires

該字段是 http1.0 時(shí)的規(guī)范,它的值為一個(gè)絕對(duì)時(shí)間的 GMT 格式的時(shí)間字符串,比如 Expires:Mon,18 Oct 2066 23:59:59 GMT。這個(gè)時(shí)間代表著這個(gè)資源的失效時(shí)間,在此時(shí)間之前,即命中緩存。這種方式有一個(gè)明顯的缺點(diǎn),由于失效時(shí)間是一個(gè)絕對(duì)時(shí)間,所以當(dāng)服務(wù)器與客戶(hù)端時(shí)間偏差較大時(shí),就會(huì)導(dǎo)致緩存混亂。

Cache-Control

Cache-Control 是 http1.1 時(shí)出現(xiàn)的 header 信息,主要是利用該字段的 max-age 值來(lái)進(jìn)行判斷,它是一個(gè)相對(duì)時(shí)間,例如 Cache-Control:max-age=3600,代表著資源的有效期是 3600 秒。cache-control 除了該字段外,還有下面幾個(gè)比較常用的設(shè)置值:

no-cache:需要進(jìn)行協(xié)商緩存,發(fā)送請(qǐng)求到服務(wù)器確認(rèn)是否使用緩存。

no-store:禁止使用緩存,每一次都要重新請(qǐng)求數(shù)據(jù)。

public:可以被所有的用戶(hù)緩存,包括終端用戶(hù)和 CDN 等中間代理服務(wù)器。

private:只能被終端用戶(hù)的瀏覽器緩存,不允許 CDN 等中繼緩存服務(wù)器對(duì)其緩存。

Cache-Control 與 Expires 可以在服務(wù)端配置同時(shí)啟用,同時(shí)啟用的時(shí)候 Cache-Control 優(yōu)先級(jí)高。

協(xié)商緩存

當(dāng)強(qiáng)緩存沒(méi)有命中的時(shí)候,瀏覽器會(huì)發(fā)送一個(gè)請(qǐng)求到服務(wù)器,服務(wù)器根據(jù) header 中的部分信息來(lái)判斷是否命中緩存。如果命中,則返回 304 ,告訴瀏覽器資源未更新,可使用本地的緩存。

這里的 header 中的信息指的是 Last-Modify/If-Modify-Since 和 ETag/If-None-Match.

Last-Modify/If-Modify-Since

瀏覽器第一次請(qǐng)求一個(gè)資源的時(shí)候,服務(wù)器返回的 header 中會(huì)加上 Last-Modify,Last-modify 是一個(gè)時(shí)間標(biāo)識(shí)該資源的最后修改時(shí)間。

當(dāng)瀏覽器再次請(qǐng)求該資源時(shí),request 的請(qǐng)求頭中會(huì)包含 If-Modify-Since,該值為緩存之前返回的 Last-Modify。服務(wù)器收到 If-Modify-Since 后,根據(jù)資源的最后修改時(shí)間判斷是否命中緩存。

如果命中緩存,則返回 304,并且不會(huì)返回資源內(nèi)容,并且不會(huì)返回 Last-Modify。

缺點(diǎn):

短時(shí)間內(nèi)資源發(fā)生了改變,Last-Modified 并不會(huì)發(fā)生變化。

周期性變化。如果這個(gè)資源在一個(gè)周期內(nèi)修改回原來(lái)的樣子了,我們認(rèn)為是可以使用緩存的,但是 Last-Modified 可不這樣認(rèn)為,因此便有了 ETag。

ETag/If-None-Match

與 Last-Modify/If-Modify-Since 不同的是,Etag/If-None-Match 返回的是一個(gè)校驗(yàn)碼。ETag 可以保證每一個(gè)資源是唯一的,資源變化都會(huì)導(dǎo)致 ETag 變化。服務(wù)器根據(jù)瀏覽器上送的 If-None-Match 值來(lái)判斷是否命中緩存。

與 Last-Modified 不一樣的是,當(dāng)服務(wù)器返回 304 Not Modified 的響應(yīng)時(shí),由于 ETag 重新生成過(guò),response header 中還會(huì)把這個(gè) ETag 返回,即使這個(gè) ETag 跟之前的沒(méi)有變化。

Last-Modified 與 ETag 是可以一起使用的,服務(wù)器會(huì)優(yōu)先驗(yàn)證 ETag,一致的情況下,才會(huì)繼續(xù)比對(duì) Last-Modified,最后才決定是否返回 304。

總結(jié)

當(dāng)瀏覽器再次訪問(wèn)一個(gè)已經(jīng)訪問(wèn)過(guò)的資源時(shí),它會(huì)這樣做:

1.看看是否命中強(qiáng)緩存,如果命中,就直接使用緩存了。

2.如果沒(méi)有命中強(qiáng)緩存,就發(fā)請(qǐng)求到服務(wù)器檢查是否命中協(xié)商緩存。

3.如果命中協(xié)商緩存,服務(wù)器會(huì)返回 304 告訴瀏覽器使用本地緩存。

4.否則,返回最新的資源。

實(shí)踐加深理解

talk is cheap , show me the code 。讓我們通過(guò)實(shí)踐得真知~

在實(shí)踐時(shí),注意瀏覽器控制臺(tái)Network的

image
按鈕不要打鉤。

以下我們只對(duì)強(qiáng)緩存的Cache-Control和協(xié)商緩存的ETag進(jìn)行實(shí)踐,其他小伙伴們可以自己實(shí)踐~

package.json

{
 "name": "webcache",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "scripts": {
   "cache": "nodemon ./index.js"
 },
 "author": "webfansplz",
 "license": "MIT",
 "devDependencies": {
   "@babel/core": "^7.2.2",
   "@babel/preset-env": "^7.2.3",
   "@babel/register": "^7.0.0",
   "koa": "^2.6.2",
   "koa-static": "^5.0.0"
 },
 "dependencies": {
   "nodemon": "^1.18.9"
 }
}

.babelrc

{
 "presets": [
   [
     "@babel/preset-env",
     {
       "targets": {
         "node": "current"
       }
     }
   ]
 ]
}

index.js

require('@babel/register');
require('./webcache.js');

webcache.js

import Koa from 'koa';
import path from 'path';
//靜態(tài)資源中間件
import resource from 'koa-static';
const app = new Koa();
const host = 'localhost';
const port = 4396;
app.use(resource(path.join(__dirname, './static')));

app.listen(port, () => {
  console.log(`server is listen in ${host}:${port}`);
});

index.html

<!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>前端緩存</title>
    <style>
      .web-cache img {
        display: block;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div class="web-cache"><img src="./web.png" /></div>
  </body>
</html>

我們用koa先起個(gè)web服務(wù)器,然后用koa-static這個(gè)中間件做靜態(tài)資源配置,并在static文件夾下放了index.html和web.png。

Ok,接下來(lái)我們來(lái)啟動(dòng)服務(wù)。

npm run cache

server is listen in localhost:4396。

接下來(lái)我們打開(kāi)瀏覽器輸入地址:

localhost:4396
image

完美~(哈哈,豬仔別噴我,純屬娛樂(lè)效果)

Ok!!!接下來(lái)我們來(lái)實(shí)踐下強(qiáng)緩存。~

Cache-Control

webcache.js

import Koa from 'koa';
import path from 'path';
//靜態(tài)資源中間件
import resource from 'koa-static';
const app = new Koa();
const host = 'localhost';
const port = 4396;

app.use(async (ctx, next) => {
 // 設(shè)置響應(yīng)頭Cache-Control 設(shè)置資源有效期為300秒
  ctx.set({
    'Cache-Control': 'max-age=300'  
  });
  await next();
});
app.use(resource(path.join(__dirname, './static')));

app.listen(port, () => {
  console.log(`server is listen in ${host}:${port}`);
});

image

我們刷新頁(yè)面可以看到響應(yīng)頭的Cache-Control變成了max-age=300。

我們順便來(lái)驗(yàn)證下三級(jí)緩存原理

我們剛進(jìn)行了網(wǎng)絡(luò)請(qǐng)求,瀏覽器把web.png存進(jìn)了磁盤(pán)和內(nèi)存中。

根據(jù)三級(jí)緩存原理,我們會(huì)先在內(nèi)存中找資源,我們來(lái)刷新頁(yè)面。

image

我們?cè)诩t線部分看到了, from memory cache。nice~

ok,接下來(lái),我們關(guān)掉該頁(yè)面,再重新打開(kāi)。因?yàn)閮?nèi)存是存在進(jìn)程中的,所以關(guān)閉該頁(yè)面,內(nèi)存中的資源也被釋放掉了,磁盤(pán)中的資源是永久性的,所以還存在。

根據(jù)三級(jí)緩存原理,如果在內(nèi)存中沒(méi)找到資源,便會(huì)去磁盤(pán)中尋找!

image

from disk cache !!! ok,以上也就驗(yàn)證了三級(jí)緩存原理,相信你對(duì)緩存資源的存儲(chǔ)也有了更深的理解了。

我們剛對(duì)資源設(shè)置的有效期是300秒,我們接下來(lái)來(lái)驗(yàn)證緩存是否失效。

300秒后。。。

image

我們通過(guò)返回值可以看到,緩存失效了。

通過(guò)以上實(shí)踐,你是否對(duì)強(qiáng)緩存有了更深入的理解了呢?

Ok!!!接下來(lái)我們來(lái)實(shí)踐下協(xié)商緩存。~

由于Cache-Control的默認(rèn)值是private(只能被終端用戶(hù)的瀏覽器緩存,不允許 CDN 等中繼緩存服務(wù)器對(duì)其緩存。),所以我們這里不用對(duì)Cache-Control進(jìn)行設(shè)置!

ETag

//ETag support for Koa responses using etag.
npm install koa-etag -D
// etag works together with conditional-get
npm install koa-conditional-get -D

我們這里直接使用現(xiàn)成的插件幫我們計(jì)算文件的ETag值,站在巨人的肩膀上!

webcache.js

import Koa from 'koa';
import path from 'path';
//靜態(tài)資源中間件
import resource from 'koa-static';
import conditional from 'koa-conditional-get';
import etag from 'koa-etag';
const app = new Koa();
const host = 'localhost';
const port = 4396;

// etag works together with conditional-get
app.use(conditional());
app.use(etag());
app.use(resource(path.join(__dirname, './static')));

app.listen(port, () => {
 console.log(`server is listen in ${host}:${port}`);
});

ok。第一次請(qǐng)求.


image

我們發(fā)現(xiàn)返回值里面已經(jīng)有了Etag值。

接下來(lái)再請(qǐng)求的時(shí)候,瀏覽器將會(huì)帶上If-None-Match請(qǐng)求頭,并賦值為上一次返回頭的Etag值,然后與 這次返回值的Etag值進(jìn)行對(duì)比。如果一致則命中協(xié)商緩存。返回304 Not Modified。接下來(lái)我們來(lái)驗(yàn)證一下~

image

ok,如圖所示,完美驗(yàn)證了上面的說(shuō)法。

接下來(lái)我們修改web.png ,來(lái)驗(yàn)證是否資源改變時(shí) 協(xié)商緩存策略也就失效呢?

image

如圖所示.協(xié)商緩存的實(shí)踐也驗(yàn)證了原理。

大功告成

寫(xiě)文章真的是件挺累的事,如果覺(jué)得有幫助到你,請(qǐng)給star/follow 支持下作者~

源碼地址

參考文獻(xiàn)

前端性能優(yōu)化之緩存利用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容