近在項目中遇到了 IE瀏覽器因緩存問題未能成功向后端發送 GET類型請求 的bug,然后順藤摸瓜順便看了看緩存的知識,覺得有必要總結跟大家分享一下。
在前端開發中,性能一直都是被大家所重視的一點,然而判斷一個網站的性能最直觀的就是看網頁打開的速度。其中提高網頁反應速度的一個方式就是使用緩存。一個優秀的緩存策略可以縮短網頁請求資源的距離,減少延遲,并且由于緩存文件可以重復利用,還可以減少帶寬,降低網絡負荷。
1. 介紹
web緩存是指一個web資源(如html頁面,圖片,js,數據等)存在于web服務器和客戶端(瀏覽器)之間的副本。
緩存會根據進來的請求保存輸出內容的副本;當下一個請求來到的時候,如果是相同的URL,緩存會根據緩存機制決定是直接使用副本響應訪問請求,還是向源服務器再次發送請求。比較常見的就是瀏覽器會緩存訪問過網站的網頁,當再次訪問這個URL地址的時候,如果網頁沒有更新,就不會再次下載網頁,而是直接使用本地緩存的網頁。只有當網站明確標識資源已經更新,瀏覽器才會再次下載網頁。至于瀏覽器和網站服務器是如何標識網站頁面是否更新的機制,將在后面介紹。
1.1 web緩存的作用
web緩存的作用顯而易見:
1.2 web緩存的類型
web緩存大致可以分為以下幾種類型 詳細內容:
瀏覽器通過代理服務器向源服務器發起請求的原理如下圖:
瀏覽器先向代理服務器發起web請求,再將請求轉發到源服務器。它屬于共享緩存,所以很多地方都可以使用其緩存資源,因此對于節省流量有很大作用。
瀏覽器緩存是將文件保存在客戶端,在同一個會話過程中會檢查緩存的副本是否足夠新,在后退網頁時,訪問過的資源可以從瀏覽器緩存中拿出使用。通過減少服務器處理請求的數量,用戶將獲得更快的體驗
下面著重關注一下瀏覽器緩存。
2. web緩存的工作原理
所有的緩存都是基于一套規則來幫助他們決定什么時候使用緩存中的副本提供服務(假設有副本可用的情況下,未被銷毀回收或者未被刪除修改)。這些規則有的在協議中有定義(如HTTP協議1.0和1.1),有的則是由緩存的管理員設置(如DBA、瀏覽器的用戶、代理服務器管理員或者應用開發者)。
2.1 瀏覽器端的緩存規則
對于瀏覽器端的緩存來講,這些規則是在HTTP協議頭和HTML頁面的 Meta標簽中定義的。他們分別從新鮮度和校驗值兩個維度來規定瀏覽器是直接使用緩存中的副本,還是需要去源服務器獲取更新的版本。
2.2 瀏覽器緩存的控制
2.2.1 使用HTML的 Meta 標簽
< META HTTP - EQUIV = "Pragma" CONTENT = "no-cache" >
上述代碼的作用是告訴瀏覽器當前頁面不被緩存,每次訪問都需要去服務器拉取。使用上很簡單,但只有部分瀏覽器可以支持,而且所有緩存代理服務器都不支持,因為代理不解析HTML內容本身。可以通過這個頁面測試你的瀏覽器是否支持:[Pragma No-Cache Test] (http://www.procata.com/cachetest/tests/pragma/index.php)。
2.2.2 使用緩存有關的HTTP消息報頭
一個URI的完整HTTP協議交互過程是由HTTP請求和HTTP響應組成的。有關HTTP詳細內容可參考《Hypertext Transfer Protocol — HTTP/1.1》、《HTTP協議詳解》等。
在HTTP請求和響應的消息報頭中,常見的與緩存有關的消息報頭有:
稍微解釋一下:
1. Cache-Control
cache-control的種類這么多,然而怎么使用它們呢,參看下圖:
2. Expires
緩存過期時間,用來指定資源到期的時間,是服務器端的具體的時間點。也就是說,Expires=max-age + 請求時間 ,需要和Last-modified結合使用。但在上面我們提到過,cache-control的優先級更高。Expires是Web服務器響應消息頭字段,在響應http請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數據,而無需再次請求。
3. Last-modified & If-modified-since
服務器端文件的最后修改時間,需要和cache-control共同使用,是檢查服務器端資源是否更新的一種方式。當瀏覽器再次進行請求時,會向服務器傳送If-Modified-Since報頭,詢問Last-Modified時間點之后資源是否被修改過。如果沒有修改,則返回碼為304,使用緩存;如果修改過,則再次去服務器請求資源,返回碼和首次請求相同為200,資源為服務器最新資源。
4. Etag & & If-None-Match
根據實體內容生成一段hash字符串,標識資源的狀態,由服務端產生。瀏覽器會將這串字符串傳回服務器,驗證資源是否已經修改,如果沒有修改,過程如下:
2.2.3 緩存報頭種類與優先級
1. Cache-Control與Expires
Cache-Control與 Expires的作用一致,都是指明當前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數據還是重新發請求到服務器取數據。只不過 Cache-Control的選擇更多,設置更細致,如果同時設置的話,其優先級高于 Expires。
2. Last-Modified與ETag
你可能會覺得使用 Last-Modified 已經足以讓瀏覽器知道本地的緩存副本是否足夠新,為什么還需要 Etag(實體標識)呢?HTTP1.1中Etag的出現主要是為了解決幾個 Last-Modified 比較難解決的問題:
Etag是服務器自動生成或者由開發者生成的對應資源在服務器端的唯一標識符,能夠更加準確的控制緩存。Last-Modified與ETag是可以一起使用的,服務器會優先驗證ETag,一致的情況下,才會繼續比對Last-Modified,最后才決定是否返回304。Etag的服務器生成規則和強弱Etag的相關內容可以參考,《互動百科-Etag》和《HTTP Header definition》,這里不再深入。
3. Last-Modified/ETag 與 Cache-Control/Expires
配置 Last-Modified/ETag的情況下,瀏覽器再次訪問統一URI的資源,還是會發送請求到服務器詢問文件是否已經修改,如果沒有,服務器會只發送一個304回給瀏覽器,告訴瀏覽器直接從自己本地的緩存取數據;如果修改過那就整個數據重新發給瀏覽器;
Cache-Control/Expires則不同,如果檢測到本地的緩存還是有效的時間范圍內,瀏覽器直接使用本地副本,不會發送任何請求。兩者一起使用時, Cache-Control/Expires的優先級要高,即當本地副本根據 Cache-Control/Expires發現還在有效期內時,則不會再次發送請求去服務器詢問修改時間 Last-Modified或實體標識 Etag了。
一般情況下,兩者會配合一起使用,因為即使服務器設置緩存時間, 當用戶點擊“刷新”按鈕時,瀏覽器會忽略緩存繼續向服務器發送請求,這時 Last-Modified/ETag將能夠很好利用304,從而減少響應開銷。
2.2.4 哪些請求不能被緩存?
無法被瀏覽器緩存的請求:
3. 使用緩存流程
一個用戶發起一個靜態資源請求的時候,瀏覽器會通過以下幾步來獲取并展示資源:
緩存行為主要由緩存策略決定,而緩存策略由內容擁有者設置。這些策略主要通過特定的HTTP頭部來清晰地表達。
以上過程也可以被概括為三個階段:
4. 用戶操作行為與緩存的關系
用戶在使用瀏覽器的時候,會有各種操作,比如輸入地址后回車,按F5刷新等,這些行為會對緩存有什么影響呢?
通過上表我們可以看到,當用戶在按 F5進行刷新的時候,會忽略Expires/Cache-Control的設置,會再次發送請求去服務器請求,而Last-Modified/Etag還是有效的,服務器會根據情況判斷返回304還是200;
而當用戶使用 Ctrl+F5進行強制刷新的時候,只是所有的緩存機制都將失效,重新從服務器拉去資源。
5. 如何從緩存角度改善站點
關注微信公眾號:安徽思恒信息科技有限公司,了解更多技術內容……
近一次移動端Vue應用的上線,導致某些用戶使用某些功能時出現問題,經主動清空緩存后恢復。有時候清空微信應用的存儲空間緩存仍不能解決問題,此時安卓機可借助微信TBS調試工具 http://debugx5.qq.com (微信中打開頁面,勾選最下面四個選項清除緩存),但該工具目前只支持安卓手機,蘋果機就比較麻煩了。為了找到問題的本質,從根本上避免問題,最近瀏覽了一些文章,其中有一篇對瀏覽器緩存的分析及在Nginx中對應的處理策略總結的比較好,這里分享給大家。
以下為原文。
關于http或者是瀏覽器緩存策略,我認為可以分為這三種:
有時,我們希望瀏覽器永遠都不要使用緩存,全部到服務器拉取數據,此時即為不使用緩存,我們可以在服務端通過Cache-Control為 no-store實現。
服務器端針對上面文件設置了no-store,可以看到在請求的時候,無論怎么刷新,都是返回200,不會顯示304,也不會顯示“memory cache”或“disk cache”,說明真的都是從服務器重新拉取數據。
比如我們想設置html文件不緩存,可以在域名的解析配置中如下設置,當文件后綴為html或htm時add_header Cache-Control "no-store"
server {
listen 80;
server_name yourdomain.com;
location / {
try_files $uri $uri/ /index.html;
root /yourdir/;
index index.html index.htm;
if ($request_filename ~* .*\.(?:htm|html)$)
{
add_header Cache-Control "no-store"; //對html文件設置永遠不緩存
}
}
}
這種方式缺點就是每次都要去服務端拉取文件,即使文件沒有更新,很明顯這樣增加了不必要的帶寬消耗。
如果文件沒有更新,我們就使用緩存,只有更新了才去拉取最新文件,這樣多好,這就是協商緩存。
協商緩存就是瀏覽器攜帶文件緩存標識(如Last-Modified或ETag),向服務器發送請求,由服務器根據文件緩存標識來決定是否使用緩存,如果文件沒有更新,則告訴瀏覽器使用本地緩存,如果文件更新了,則直接返回新文件內容。
可以看出,相比不使用緩存,協商緩存是會大大減少帶寬消耗的。
我們在瀏覽器調試頁面,可以看到有304的,即是使用了協商緩存
服務器返回的header中會有Last-Modified和ETag標識,而瀏覽器請求header中會包含If-Modified-Since和If-None-Match
在 http 1.0 版本中,第一次請求資源時服務器通過 Last-Modified 來設置響應頭的緩存標識,并且把資源最后修改的時間作為值填入,然后將資源返回給瀏覽器。在第二次請求時,瀏覽器會首先帶上 If-Modified-Since 請求頭去訪問服務器,服務器會將 If-Modified-Since 中攜帶的時間與資源修改的時間匹配,如果時間不一致,服務器會返回新的資源,并且將 Last-Modified 值更新,作為響應頭返回給瀏覽器。如果時間一致,表示資源沒有更新,服務器返回 304 狀態碼,瀏覽器拿到響應狀態碼后從本地緩存數據庫中讀取緩存資源。
這種方式有2個弊端,第一個就是當服務器中的資源增加了一個字符,后來又把這個字符刪掉,本身資源文件并沒有發生變化,但修改時間發生了變化。當下次請求過來時,服務器也會把這個本來沒有變化的資源重新返回給瀏覽器;第二個就是修改時間的單位為秒,所以存在1s的間隙,即使更新了,也會認為沒有更新。
在 http 1.1 版本中,服務器通過 Etag 來設置響應頭緩存標識。Etag 的值由服務端生成,可以認為是文件內容的hash值。在第一次請求時,服務器會將資源和 Etag 一并返回給瀏覽器,瀏覽器將兩者緩存到本地緩存數據庫。在第二次請求時,瀏覽器會將 Etag 信息放到 If-None-Match 請求頭去訪問服務器,服務器收到請求后,會將服務器中的文件標識與瀏覽器發來的標識進行對比,如果不相同,服務器返回更新的資源和新的 Etag ,如果相同,服務器返回 304 狀態碼,瀏覽器讀取緩存。
可以在服務端通過設置Cache-Control為 no-cache或者max-age=0來實現
有時我們希望文件強制使用緩存,比如通過vue-cli產生的js和css,文件名上帶有hash值,所以如果文件名沒有變的時候,我們希望文件永久緩存,這樣可以減少網絡請求。
強制緩存整體流程比較簡單,就是在第一次訪問服務器取到數據之后,在過期時間之內不會再去重復請求。實現這個流程的核心就是如何知道當前時間是否超過了過期時間。
強制緩存的過期時間通過第一次訪問服務器時返回的響應頭獲取。在 http 1.0 和 http 1.1 版本中通過不同的響應頭字段實現。
在 http 1.0 版本中,強制緩存通過 Expires 響應頭來實現。 expires 表示未來資源會過期的時間。也就是說,當發起請求的時間超過了 expires 設定的時間,即表示資源緩存時間到期,會發送請求到服務器重新獲取資源。而如果發起請求的時間在 expires 限定的時間之內,瀏覽器會直接讀取本地緩存數據庫中的信息(from memory or from disk),兩種方式根據瀏覽器的策略隨機獲取。
在 http 1.1 版本中,可以設置Cache-Control中的 max-age=xxx ,來表示緩存的資源將在 xxx 秒后過期。一般來說,為了兼容,兩個版本的強制緩存都會被實現。
為什么有了Expires,后來又增加了max-age呢,這是因為Expires是一個絕對時間,有可能客戶端的時間和服務器不一致,導致緩存不能按照預期進行,而max-age則是個相對時間,比如3600s,自瀏覽器請求后3600s之內,都使用本地緩存,和客戶端的時間沒關系。
由于打包后的js、css和圖片,一般名稱都帶有hash值,名稱中的hash變了,自然會拉取新文件,所以我們可以將這類文件設置為強制緩存,只要文件名不變,就一直緩存,比如緩存100天或者一年。
而html文件則不能設為強制緩存,一般html名稱是沒法帶hash值的,所以html如果設置了強制緩存,則永遠也沒法更新,html不更新,其引用的js、css等名稱也不會更新,則整個服務都沒有更新,只能讓用戶清除緩存了。所以針對html文件,我們可以設置協商緩存或者直接不使用緩存,本身html文件都比較小,我是直接使用了不緩存,nginx配置如下。
server {
listen 80;
server_name yourdomain.com;
location / {
try_files $uri $uri/ /index.html;
root /yourdir/;
index index.html index.htm;
if ($request_filename ~* .*\.(js|css|woff|png|jpg|jpeg)$)
{
expires 100d; //js、css、圖片緩存100天
#add_header Cache-Control "max-age = 8640000"; //或者設置max-age
}
if ($request_filename ~* .*\.(?:htm|html)$)
{
add_header Cache-Control "no-store"; //html不緩存
}
}
}
歡迎微信搜索關注公眾號:半路雨歌,查看更多技術干貨文章
緩存,這個問題在面試中出現的頻率還是很高的,也是我們日常工作中必知必會的。
所以在本文中,我們來嘗試對HTTP 協議中規定的很多請求頭和響應頭來做總結和歸納,讓大家了解到瀏覽器是如何通過他們來控制緩存的。
下圖是一個經典的 GET 請求的處理過程:
當一個請求達到時,瀏覽器會先檢查被訪問的資源是否已被緩存。
如果未被緩存(緩存未命中 cache miss),則將請求轉發給原始服務器。
如果已被緩存(緩存命中,cache hit),則會檢查緩存是否足夠新鮮。
如果緩存的副本足夠新鮮,則直接將副本返回給客戶端,否則會向服務端發起新鮮度驗證(revalidation)。如果發現與服務端文件一致,則將本地緩存副本返回給客戶端,否則將請求轉發給原始服務器。
在這個過程中,由緩存提供的服務在所有請求占比中的比例稱為緩存命中率(cache hit rat)。
這種描述方式只能描述請求級別的命中情況,無法體現具體有多少流量來自緩存。
比如一個訪問頻次很低,尺寸又很大的文件,如果以該命中率來描述的話,命中率非常低。但是這個文件卻占據了絕大多數的訪問流量。
因此還需要另一個命中率指標來描述,那就是字節命中率(byte hit rate)。字節命中率表示的是緩存提供的字節在所有傳輸字節中的占比。
瀏覽器常用校驗緩存的機制有兩種:
下面我們來分別介紹下這兩種機制。
HTTP 通過 Cache-Control:max-age 和 Expires 這兩個響應頭信息, 讓原始服務器向每個文檔附加一個“過期日期”。在緩存文檔過期之前,緩存可以以任意頻次使用這些副本,而無需與服務端聯系。
Expires 首部與 Cache-Control:max-age 首部本質上是一樣的,區別是 Expires 是 HTTP/1.0 協議規定的首部,且首部取值為一個絕對時間,在這個時間之后緩存失效;Cache-Control:max-age 是 HTTP/1.1 協議規定的首部,且首部取值是一個相對時間,單位為秒。
一個絕對,一個相對。
HTTP 定義了 5 個條件請求首部來完成服務器再驗證:
其中最有用的是 If-Modified-Since 和 If-None-Match 兩個首部。
一、If-Modified-Since: Date 再驗證
If-Modified-Since: Date 再驗證請求流程分兩種:
1、自指定日期后,文檔被修改了,If-Modified-Since 條件為真,GET 請求就會執行。攜帶新首部的新文檔會被返回給緩存,新首部除了其他信息以外,還包含了一個新的過期日期。
2、自指定日期后,文檔沒有被修改過,If-Modified-Since 條件為假,則會向客戶端返回一個304 Not Modified 響應報文,為了提高有效性,一般會發送一個新的過期日期,不會返回文檔的主體。
If-Modified-Since 請求首部通常與 Last-Modified 服務器響應首部配合工作。原始服務器會將最后的修改日期附加到文檔上去。當瀏覽器要對已緩存的文檔進行再驗證時,就會包含一個 If-Modified-Since首部,其中攜帶有最后修改已緩存副本的日期:
If-Modified-Since: <cached last-modified date>
這里以W3C網站為例:
從上面兩張圖上,可以分別看到請求頭信息中的 If-Modified-Since 和響應頭信息中的 Last-Modified。
說白了,就是本地記錄的修改日期,與服務器那邊記錄的修改日期做對比。如果服務的比較新,就重新過去,否則加載本地的。
上圖我第一次進來,因為本地并沒有If-Modified-Since 記錄,所以只能看到響應頭中有最后修改時間信息,以及返回的200狀態碼。
當我再次刷新網頁之后:
瞧,請求頭也有信息了。因為If-Modified-Since 和 Last-Modified兩個值是一樣的,所以服務器校驗本頁面為緩存,返回304狀態碼。
在某些情況下,If-Modified-Since: Date 可能無法很好地解決緩存問題。
比如一個被周期性復寫的文件,雖然修改日期每次都有變化,但是文件的內容往往是一樣的,如果此時還根據最后修改時間去判定是否為緩存,顯示是不合適的。
這種情況下,就需要借助實體標簽(Etag)驗證了。
實體標簽就是“版本標識符”,是附加到文檔上的任意標簽(引用字符串),可能包含了文檔的序列號或版本名,或者是文檔內容的校驗信息。
If-None-Match: etag 實體標簽驗證的工作過程與 If-Modified-Since: Date 再驗證的工作過程基本一致,不同的是,服務器會在響應中附加一個 Etag 響應頭。當緩存要對已緩存的文檔進行再驗證時,就會將這個 etag 放到 If-None-Match 請求頭中去。
還是以上面的請求為例:
可以看到,If-none-match 在chrome網絡面板中的位置,就是在 If-modifed-since 后面,只是它對應的不是修改日期,而是上面相應的etag罷了。
說簡單點,就是hash字符串校驗。
注意:如果你是第一次訪問該頁面,請求頭中同樣沒有 if-none-match 信息。
一、服務器端響應
服務端通過Cache-Control 來對響應緩存做限制,以下控制優先級按順序依次遞減:
Cache-Control: no-store 禁止緩存對響應進行復制。
Cache-Control: no-cache/ Pragma: no-cache 緩存可以復制響應,但是在與原始服務器進行新鮮度再驗證之前不能將其提供給客戶端。Pramga: no-cache 為了兼容 HTTP/1.0,優先級低于 Cache-Control: no-cache。
Cache-Control: must-revalidate 在事前沒有跟原始服務器進行再驗證的情況下,緩存不能提供緩存副本。
Cache-Control: max-age max-age 指定的秒數內有效。max-age 為零時,不可緩存。
Expires: Date 在實際的絕對日期之前有效。
二、客戶端請求
客戶端同樣可以通過 Cache-Control 請求首部來強化或放松對過期時間的限制。以下是使用時的具體參數及說明:
Cache-Control: max-stale=< s > 緩存可以隨意提供副本,如果指定的秒數,那么在這段時間內,文檔不能過期。
Cache-Control: min-fresh=< s > 至少在未來< s >秒內文檔保持新鮮。
Cache-Control: max-age=< s > 緩存無法返回緩存時間超過< s >的文檔。如果與 max-stale 通用,max-stale 優先級更高。
Cache-Control: no-cache/Pragma: no-cache 除非進行了再驗證,否則客戶端不接受已緩存的資源。
Cache-Control: no-store 緩存應該刪除本地緩存副本,使用原始服務器響應。
Cache-Control: only-if-cache 只有當緩存中有副本存在時,客戶端才會獲取一份副本。
好了,緩存就暫時先說到這里。最后讓我們再來看下瀏覽器的緩存執行流程,鞏固一下吧。
瀏覽器首次請求:
瀏覽器再次請求時:
*請認真填寫需求信息,我們會在24小時內與您取得聯系。