中高級前端工程師都需要熟悉的技能 -- 前端緩存
作者:工邊頁字
原文:https://juejin.cn/post/7127194919235485733
一、 前言
文章會盡量用通俗易懂的言語來細說 web 緩存的概念和用處。
本期文章的大綱是
-
什麼是 web 緩存(前端緩存)
-
緩存可以解決什麼問題?他的缺點是什麼?
-
強制緩存原理講解
3.1. 基於 Expires 字段實現的強緩存
3.2. 基於 Cache-control 實現的強緩存
-
協商緩存原理講解
4.1. 基於 last-modified 實現的協商緩存
4.2. 基於 ETag 實現的協商緩存
二、 什麼是 web 緩存?
web 緩存主要指的是兩部分:瀏覽器緩存和 http 緩存。
其中 http 緩存是 web 緩存的核心,是最難懂的那一部分, 也是最重要的那一部分。
瀏覽器緩存:比如, localStorage,sessionStorage,cookie 等等。這些功能主要用於緩存一些必要的數據,比如用戶信息。比如需要攜帶到後端的參數。亦或者是一些列表數據等等。
不過這裏需要注意。像 localStorage,sessionStorage 這種用戶緩存數據的功能,他只能保存 5M 左右的數據,多了不行。cookie 則更少,大概只能有 4kb 的數據。不要擔心,這些概念對於未來會稱爲前端大牛的你來說都不是什麼問題,非常的簡單。因爲太簡單,數據緩存不再這篇文章的介紹中,這裏一筆帶過,需要了解的小夥伴,可以移步我的另一篇文章前端新能優化篇之 localStorage 和 sessionStorage 的區別及其使用方式 - 掘金 (juejin.cn)[1]。
這篇文章重點講解的是:前端 http 緩存。
http 緩存
官方介紹: Web 緩存是可以自動保存常見文檔副本的 HTTP 設備。當 Web 請求抵達緩存時, 如果本地有 “已緩存的” 副本,就可以從本地存儲設備而不是原始服務器中提取這 個文檔。
舉個例子↓
看圖,問題就是出在,服務器需要處理 http 的請求,並且 http 去傳輸數據,需要帶寬,帶寬是要錢買的啊。而我們緩存,就是爲了讓服務器不去處理這個請求,客戶端也可以拿到數據。
注意,我們的緩存主要是針對 html,css,img 等靜態資源,常規情況下,我們不會去緩存一些動態資源,因爲緩存動態資源的話,數據的實時性就不會不太好,所以我們一般都只會去緩存一些不太容易被改變的靜態資源。
三、 緩存可以解決什麼問題?他的缺點是什麼?
先說說,緩存可以解決什麼問題。
-
減少不必要的網絡傳輸,節約寬帶(就是省錢)
-
更快的加載頁面(就是加速)
-
減少服務器負載,避免服務器過載的情況出現。(就是減載)
再說說缺點
- 佔內存(有些緩存會被存到內存中)
其實日常的開發中,我們最最最最關心的,還是 "更快的加載頁面"; 尤其是對於 react/vue 等 SPA(單頁面)應用來說,首屏加載是老生常談的問題。這個時候,緩存就顯得非常重要。不需要往後端請求,直接在緩存中讀取。速度上,會有顯著的提升。是一種提升網站性能與用戶體驗的有效策略。
http 緩存又分爲兩種兩種緩存,強制緩存和協商緩存, 我們來深度剖析一下強制緩存和協商緩存各自的優劣以及他們的使用場景以及使用原理
http 緩存流程圖↓
四、 強制緩存
強制緩存,我們簡稱強緩存。
從強制緩存的角度觸發,如果瀏覽器判斷請求的目標資源有效命中強緩存,如果命中,則可以直接從內存中讀取目標資源,無需與服務器做任何通訊。
基於 Expires 字段實現的強緩存
在以前,我們通常會使用響應頭的Expires
字段去實現強緩存。如下圖↓
Expires
字段的作用是,設定一個強緩存時間。在此時間範圍內,則從內存(或磁盤)中讀取緩存返回。
比如說將某一資源設置響應頭爲: Expires:new Date("2022-7-30 23:59:59");
那麼,該資源在 2022-7-30 23:59:59 之前,都會去本地的磁盤(或內存)中讀取,不會去服務器請求。
但是,**Expires
已經被廢棄了 **。對於強緩存來說,Expires
已經不是實現強緩存的首選。
因爲 Expires 判斷強緩存是否過期的機制是: 獲取本地時間戳,並對先前拿到的資源文件中的**Expires
字段的時間做比較。來判斷是否需要對服務器發起請求。這裏有一個巨大的漏洞:“如果我本地時間不準咋辦?”**
基於 Cache-control 實現的強緩存(代替 Expires 的強緩存實現方法)
Cache-control
這個字段在 http1.1 中被增加,Cache-control
完美解決了Expires
本地時間和服務器時間不同步的問題。是當下的項目中實現強緩存的最常規方法。
Cache-control
的使用方法頁很簡單,只要在資源的響應頭上寫上需要緩存多久就好了,單位是秒。比如↓
//往響應頭中寫入需要緩存的時間
res.writeHead(200,{
'Cache-Control':'max-age=10'
});
Cache-Control:max-age=N,N 就是需要緩存的秒數。從第一次請求資源的時候開始,往後 N 秒內,資源若再次請求,則直接從磁盤(或內存中讀取),不與服務器做任何交互。
Cache-control
中因爲 max-age 後面的值是一個滑動時間,從服務器第一次返回該資源時開始倒計時。所以也就不需要比對客戶端和服務端的時間,解決了Expires
所存在的巨大漏洞。
Cache-control
有 max-age、s-maxage、no-cache、no-store、private、public 這六個屬性。
-
max-age 決定客戶端資源被緩存多久。
-
s-maxage 決定代理服務器緩存的時長。
-
no-cache 表示是強制進行協商緩存。
-
no-store 是表示禁止任何緩存策略。
-
public 表示資源即可以被瀏覽器緩存也可以被代理服務器緩存。
-
private 表示資源只能被瀏覽器緩存。
no-cache 和 no-store
no_cache 是Cache-control
的一個屬性。它並不像字面意思一樣禁止緩存,實際上,no-cache 的意思是強制進行協商緩存。如果某一資源的Cache-control
中設置了 no-cache,那麼該資源會直接跳過強緩存的校驗,直接去服務器進行協商緩存。而 no-store 就是禁止所有的緩存策略了。
注意,no-cache 和 no-store 是一組互斥屬性,這兩個屬性不能同時出現在
Cache-Control
中。
public 和 private
一般請求是從客戶端直接發送到服務端,如下↓
但有些情況下是例外的:比如,出現代理服務器,如下↓
而 public 和 private 就是決定資源是否可以在代理服務器進行緩存的屬性。
其中,public 表示資源在客戶端和代理服務器都可以被緩存。
private 則表示資源只能在客戶端被緩存,拒絕資源在代理服務器緩存。
如果這兩個屬性值都沒有被設置,則默認爲 private
注意,public 和 private 也是一組互斥屬性。他們兩個不能同時出現在響應頭的
cache-control
字段中。
max-age 和 s-maxage
max-age 表示的時間資源在客戶端緩存的時長,而 s-maxage 表示的是資源在代理服務器可以緩存的時長。
在一般的項目架構中 max-age 就夠用。
注意,max-age 和 s-maxage 並不互斥。他們可以一起使用。
那麼, Cache-control 如何設置多個值呢?用逗號分割,如下↓
Cache-control:max-age=10000,s-maxage=200000,public
強制緩存就是以上這兩種方法了。現在我們回過頭來聊聊,****Expires
難道就一點用都沒有了嗎?也不是,雖然Cache-control是Expires
的完全替代品,但是如果要考慮向下兼容的話,在Cache-control
不支持的時候,還是要使用Expires
,這也是我們當前使用的這個屬性的唯一理由。
五、 協商緩存
溫馨提示: 協商緩存的內容會有一點點繞。需要仔細閱讀。
基於 last-modified 的協商緩存
基於 last-modified 的協商緩存實現方式是:
-
首先需要在服務器端讀出文件修改時間,
-
將讀出來的修改時間賦給響應頭的
last-modified
字段。 -
最後設置
Cache-control:no-cache
三步缺一不可。
如下圖↓
注意圈出來的三行。
第一行,讀出修改時間。
第二行,給該資源響應頭的last-modified
字段賦值修改時間
第三行,給該資源響應頭的Cache-Control
字段值設置爲: no-cache.(上文有介紹,Cache-control:no-cache 的意思是跳過強緩存校驗,直接進行協商緩存。)
還沒完。到這裏還無法實現協商緩存
當客戶端讀取到last-modified
的時候,會在下次的請求標頭中攜帶一個字段:If-Modified-Since
。
而這個請求頭中的If-Modified-Since
就是服務器第一次修改時候給他的時間,也就是上圖中的
那麼之後每次對該資源的請求,都會帶上**If-Modified-Since
這個字段,而務端就需要拿到這個時間並再次讀取該資源的修改時間,讓他們兩個做一個比對來決定是讀取緩存還是返回新的資源。**
如圖↓
這樣,就是協商緩存的所有操作了。
看到這裏,有些小夥伴可能有些迷糊了。
沒關係,我們用一張圖來解釋下協商緩存。
使用以上方式的協商緩存已經存在兩個非常明顯的漏洞。這兩個漏洞都是基於文件是通過比較修改時間來判斷是否更改而產生的。
1. 因爲是更具文件修改時間來判斷的,所以,在文件內容本身不修改的情況下,依然有可能更新文件修改時間(比如修改文件名再改回來),這樣,就有可能文件內容明明沒有修改,但是緩存依然失效了。
2. 當文件在極短時間內完成修改的時候(比如幾百毫秒)。因爲文件修改時間記錄的最小單位是秒,所以,如果文件在幾百毫秒內完成修改的話,文件修改時間不會改變,這樣,即使文件內容修改了,依然不會 返回新的文件。
爲了解決上述的這兩個問題。從**http1.1
開始新增了一個頭信息,ETag
(Entity 實體標籤)**
又來新東西了,兄弟們頂住
基礎 ETag 的協商緩存
不用太擔心,如果你已經理解了上面比較時間戳形式的協商緩存的話,ETag
對你來說不會有難度。
ETag
就是將原先協商緩存的比較時間戳的形式修改成了比較文件指紋。
文件指紋: 根據文件內容計算出的唯一哈希值。文件內容一旦改變則指紋改變。
我們來看一下流程↓
-
第一次請求某資源的時候,服務端讀取文件並計算出文件指紋,將文件指紋放在響應頭的
etag
字段中跟資源一起返回給客戶端。 -
第二次請求某資源的時候,客戶端自動從緩存中讀取出上一次服務端返回的
ETag
也就是文件指紋。並賦給請求頭的if-None-Match
字段,讓上一次的文件指紋跟隨請求一起回到服務端。 -
服務端拿到請求頭中的
is-None-Match
字段值(也就是上一次的文件指紋),並再次讀取目標資源並生成文件指紋,兩個指紋做對比。如果兩個文件指紋完全吻合,說明文件沒有被改變,則直接返回 304 狀態碼和一個空的響應體並 return。如果兩個文件指紋不吻合,則說明文件被更改,那麼將新的文件指紋重新存儲到響應頭的ETag
中並返回給客戶端
代碼圖例↓
流程示例圖↓
從校驗流程上來說,協商緩存的修改時間比對和文件指紋比對,幾乎是一樣的。
ETag 也有缺點
-
ETag 需要計算文件指紋這樣意味着,服務端需要更多的計算開銷。。如果文件尺寸大,數量多,並且計算頻繁,那麼 ETag 的計算就會影響服務器的性能。顯然,ETag 在這樣的場景下就不是很適合。
-
ETag 有強驗證和弱驗證,所謂將強驗證,ETag 生成的哈希碼深入到每個字節。哪怕文件中只有一個字節改變了,也會生成不同的哈希值,它可以保證文件內容絕對的不變。但是,強驗證非常消耗計算量。ETag 還有一個弱驗證,弱驗證是提取文件的部分屬性來生成哈希值。因爲不必精確到每個字節,所以他的整體速度會比強驗證快,但是準確率不高。會降低協商緩存的有效性。
值得注意的一點是,不同於
cache-control
是expires
的完全替代方案 (說人話: 能用cache-control
就不要用expiress
)。ETag
並不是last-modified
的完全替代方案。而是last-modified
的補充方案(說人話:項目中到底是用ETag
還是last-modified
完全取決於業務場景,這兩個沒有誰更好誰更壞)。
追加
有掘友說👇
我來補足一下。
六、 如何設置緩存
從前端的角度來說:
你什麼都不用幹,緩存是緩存在前端,但實際上代碼是後端的同學來寫的。如果你需要實現前端緩存的話啊,通知後端的同學加響應頭就好了。
從後端的角度來說
請參考文章,雖然文章裏的後端是使用 node.js 寫的,但我寫了詳細的註釋。對於後端的同學來說。應該不難看懂。
七、 哪些文件對應哪些緩存
這個,我確實忘了說。哈哈哈。
有哈希值的文件設置強緩存即可。沒有哈希值的文件(比如 index.html)設置協商緩存
爲什麼有哈希值的文件設置強緩存
這是我打完包之後的 css 文件。大家是否注意到。我劃了紅線的部分。明顯,這絕不是我的文件名。這串和亂碼一樣的字符串叫哈希值
。每次打包之後都會生產一串新的哈希值
並追加到我們的文件名中。哈希值是打包後的文件名的一部分。
我們給 css 設置強緩存,哪怕緩存 1W 年。只要我們重新打包,生產新的哈希值。那麼文件名就更改了。對於機器來說,更改了文件名的文件,就是一個新的文件。
舉個例子👇
比如,有一個 css 文件 a1
第一次打包 a1.css 文件追加哈希值變成了 a1.aaaaa.css, 我們給 a1.aaaaa.css 設置了強緩存 1W 年。
然後項目改動,我們又打包了一次。打包後生產新的哈希值,a1.aaaaa.css 變成了 a1.bbbbb.css 文件。那麼當我們第一次訪問 a1.bbbbb.css 文件的時候是不會被緩存。因爲 1W 年的緩存是給 a1.aaaaa.css 文件做的。關我 a1.bbbbb.css 文件什麼事?這樣我們也就能拿到最新的改動。
其他可以被 webpack 生成哈希值的文件同理。
爲什麼 index.html 使用協商緩存
既然 img/css 這些文件都可以用強緩存。通過更改文件名的方式來獲取最新的數據,爲什麼我堂堂 index.html 就要用協商呢?
我給大家看個圖
因爲一般情況下,index.html 是不會設置哈希值的。(具體得看自己項目下的 dist 文件夾)
注意:哈希值是需要 webpack 生成的。不是天生的。不過有些框架會自帶(比如我使用的 umi.js), 設置緩存前務必看下自己的 dist 文件。因爲如果沒有配置的話,你可能所有文件都不帶哈希值。
總結一下
-
http 緩存可以減少寬帶流量,加快響應速度。
-
關於強緩存,
cache-control
是Expires
的完全替代方案,在可以使用cache-control
的情況下不要使用expires
-
關於協商緩存,
etag
並不是last-modified
的完全替代方案,而是補充方案,具體用哪一個,取決於業務場景。 -
有些緩存是從磁盤讀取,有些緩存是從內存讀取,有什麼區別?答:從內存讀取的緩存更快。
-
所有帶 304 的資源都是協商緩存,所有標註(從內存中讀取 / 從磁盤中讀取)的資源都是強緩存。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_jql3nqYXnwWAVUBEMdOYA