博客 / 詳情

返回

瀏覽器緩存原理

本文可以配合本人錄製的視頻一起食用

目的

通常説到瀏覽器緩存,大多是和性能優化有關,使用緩存,通常是兩個主要目的,第一是提高訪問速度,第二是減少網絡IO消耗。

當合理配置了緩存,可以得到提升用户體驗、減輕服務器負擔、節省帶寬等效果,這是一種效果顯著的前端性能優化手段。

四個方面

瀏覽器緩存機制涉及四個方面,按照獲取資源時請求的優先級排序如下:

  • Memory Cache
  • Service Worker Cache
  • HTTP Cache
  • Push Cache

其中最後一個Push Cache是HTTP2的新特性

緩存策略

通常我們面試時説的瀏覽器緩存原理,指的是緩存策略。

緩存策略分為兩類:強緩存和協商緩存,通過設置HTTP HEADER來實現。

強緩存

強緩存優先級較高,在命中強緩存失敗的情況下,才會走協商緩存。

當我們發起請求,去請求一個資源時,瀏覽器會去讀取HTTP Header中的expires或Cache-control,來判斷目標資源是否命中強緩存。

使用expires還是Cache-control和我們的HTTP協議版本有關,當使用1.0版本就根據expires進行判斷,當使用1.1就根據Cache-control進行判斷。

如果命中了強緩存,就直接從緩存中獲取資源,不與服務端發生通信。此時返回的HTTP狀態碼為200,但會有from memory cache的標識。

【判斷資源是否過期

瀏覽器是如何根據expires和Cache-control進行判斷的呢?

首先看expires,expires是一個時間戳,指的是資源過期時間,當我們試圖再次從服務器請求同一份資源時,瀏覽器就會先對比本地時間和expires的值,如果本地時間小於expires設置的過期時間,就直接從緩存中獲取資源。

因為expires的使用依賴於本地時間,這就會存在問題,不同的本地時間會使緩存的失效時間無法保證,也就是服務端和客户端會存在時差。

因此HTTP 1.1增加了Cache-control作為expires的完全替代方案,現在依然使用expires的唯一目的是為了向下兼容。兩者同時使用時,Cache-control的優先級更高。

在Cache-control中我們可以設置max-age來控制資源的有效期,這是一個時間長度,規避了時間戳帶來的問題,客户端會記錄請求到資源的時間點,以此作為相對時間的起點,確保參與計算的兩個時間節點都來源於客户端。

協商緩存

如果強緩存命中失敗,瀏覽器就需要向服務器去詢問緩存的相關信息,進而判斷是重新請求、下載完整的響應,還是從本地中獲取緩存的資源,此時就進入了協商緩存的階段。

如果我們不想直接使用本地緩存,也可以設置Cache-Control: no-cache直接進入協商緩存的階段。

協商緩存,從字面上可以理解為:根據客户端和服務器的協商結果,來判斷是否使用本地緩存。

如果服務器提示目標資源未改動(Not Modified),資源會被重定向到瀏覽器緩存,這種情況下網絡請求對應的狀態碼是304。

【判斷資源有效性

那麼服務器是如何去判斷資源是否變動呢?

有兩種判斷方式:Last-Modified和Etag

首先看Last-Modified,它是一個時間戳,如果啓用了協商緩存,它會在請求時隨着response headers返回。

Last-Modified:

之後我們每次請求時,請求頭request headers中就會帶上一個時間戳字段,叫If-Modified-Since,它的值就是之前response返回的Last-Modified的值。字面上的意思就是在告訴服務器:在這個時間之後,資源如果發生變化,就要返回新的資源

服務器接收到這個字段後,就會與該資源在服務器上的最後修改時間進行比對,兩者是否一致,從而判斷資源是否發生變化。

  • 如果發生變化

    會返回一個完整的響應。並在響應頭response headers中返回新的Last-Modified的值

  • 如果沒有發生變化

    就會返回一個狀態碼為304的響應,提示瀏覽器使用本地緩存

Last-Modified和If-Modified-Since配合使用,看上去沒什麼問題,但實際存在一些弊端,比如:

  • 編輯了文件,但文件內容實際沒有變化的情況

    我可能修改了一個文件,然後又撤銷了修改,雖然文件內容沒變,但資源的最後修改時間被更新了。

    導致需要重新將資源返回給客户端,無法充分利用緩存

  • 第二種情況,修改文件的速度過快,導致客户端使用了過期緩存

    因為If-Modified-Since只能檢查以秒為最小計量單位的時間差,如果在不可感知的時間內修改了資源,就會使客户端使用過期的資源

為了彌補Last-Modified的不足,就出現了Etag

Etag是服務器為每個資源生成的唯一標識字符串,基於文件內容編碼,能夠精準感知文件內容變化

首次請求時,客户端會在response headers獲得一個標識字符串,類似:

Etag: "dec8d6f46497a9c6b4dff4e237cabe5d"

再次請求時,請求頭request headers裏會帶上一個與Etag值相同的、名為If-None-Match的字段,字面上的意思就是在請求時告訴服務器:資源的Etag如果不一致,就要返回新的資源

If-None-Match的值就是供服務器進行比對:

If-None-Match:  "dec8d6f46497a9c6b4dff4e237cabe5d"

Etag很好的彌補了Last-Modified的短板,但是也存在弊端,因為生成Etag需要服務器付出額外的開銷,這會影響服務器的性能,這是啓用Etag時需要考慮到的問題。

當Etag和Last-Modified同時存在時,Etag的優先級更高,因為它在感知文件變化上比Last-Modified更準確

如何配置?

瞭解了緩存策略,那在面對一個具體的緩存需求時,我們該如何配置呢?

HTTP緩存流程

網上這一張應該挺多人看到過的圖,我也直接拿過來用,就是根據你項目的需求進行判斷

  • 考慮這個響應是否是可複用的,也就是是否使用緩存

    • 如果是No,也就是不可複用,就設置no-store,拒絕一切形式的緩存,包括客户端和服務器的緩存

      Cache-Control: no-store
    • 如果是可複用響應

      • 並且需要每次都進行資源有效性驗證,就設置no-cache,會跳過強緩存判斷,直接進入協商緩存階段,向服務器進行資源有效性驗證

        Cache-Control: no-cache
      • 考慮是否可被代理服務器緩存

        • 如果只能被瀏覽器緩存,就設置為private,這也是默認值
        Cache-Control: private
        • 如果即可被瀏覽器緩存,也能被代理服務器緩存,就設置為pulic
        Cache-Control: public
      • 繼續我們可以考慮設置緩存有效時間,以保證緩存的有效性

        • 設置代理服務器緩存的有效時間,使用s-maxage,單位為秒
        Cache-Control: public s-maxage=30
        • 設置瀏覽器緩存的有效時間,使用max-age,單位也為秒
        Cache-Control: max-age=30

        s-maxage優先級高於max-age,如果s-maxage未過期,則向代理服務器請求其緩存內容,只針對public緩存有效。

      • 最後設置協商緩存需要用到的ETag、Last-Modified等參數

這個圖大概就是這麼一個流程。

實操

以上大多都是我查到的一些資料,接下來做一些簡單的配置,以科學的精神驗證一部分內容。

下面的測試在nginx和Chrome中進行。

當不做配置時

location /cache-demo {
    root   html;
    index  index.html;
}
  • 第一次訪問頁面

    // 響應碼 200
    Status Code: 200 OK
    // 響應頭裏加上了Last-Modified和Etag
    Last-Modified: Thu, 27 Jul 2023 02:47:55 GMT
    ETag: "64c1dadb-e3"
    // 第一次請求頭裏沒有`If-Modified-Since`和`If-None-Match`
  • 再次請求,status變成了304,説明資源內容沒有更改

    // 響應碼304
    Status Code: 304 Not Modified
    // 響應頭裏Last-Modified和Etag和之前一次是一樣的
    Last-Modified: Thu, 27 Jul 2023 02:47:55 GMT
    ETag: "64c1dadb-e3"
    // 這一次請求頭裏帶上了`If-Modified-Since`和`If-None-Match`
    If-Modified-Since: Thu, 27 Jul 2023 02:47:55 GMT
    If-None-Match: "64c1dadb-e3"
  • 此時我們去修改資源內容,再去重新訪問,響應碼重新變成了200

    // 響應
    HTTP/1.1 200 OK
    Last-Modified: Thu, 27 Jul 2023 03:50:18 GMT
    ETag: "64c1e97a-e3"
    // 請求頭
    If-Modified-Since: Thu, 27 Jul 2023 02:47:55 GMT
    If-None-Match: "64c1dadb-e3"

    可以看到響應頭中的Etag和請求頭中的If-None-Match不一致,Last-Modified和If-Modified-Since也不一致了,所以就返回了新的資源內容

配置no-store

接下來先做第一種配置,Cache-Control設置為no-store

add_header 'Cache-Control' 'no-store';
  • 第一次訪問

    // 響應
    HTTP/1.1 200 OK
    Last-Modified: Thu, 27 Jul 2023 03:50:18 GMT
    ETag: "64c1e97a-e3"
    // 在響應頭中看到了no-store的配置
    Cache-Control: no-store
    // 請求頭裏沒有`If-Modified-Since`和`If-None-Match`
  • 再次請求,可以看到status還是200,説明從服務器重新獲取資源內容了

    // 響應
    HTTP/1.1 200 OK
    Last-Modified: Thu, 27 Jul 2023 03:50:18 GMT
    ETag: "64c1e97a-e3"
    Cache-Control: no-store
    // 請求
    // 請求頭裏沒有`If-Modified-Since`和`If-None-Match`,只有Cache-Control:max-age=0
    Cache-Control: max-age=0
  • 再請求幾次結果也是一樣的

配置max-age

修改配置max-age=240,也就是4分鐘內如果命中緩存,就使用緩存

add_header 'Cache-Control' 'max-age=240';
  • 第一次請求資源

    // 響應頭
    HTTP/1.1 200 OK
    Last-Modified: Thu, 27 Jul 2023 04:13:31 GMT
    ETag: "64c1eeeb-e3"
    Cache-Control: max-age=240
    // 請求
    // 請求頭裏沒有`If-Modified-Since`和`If-None-Match`
  • 四分鐘內再次請求,status是304

    // 響應頭
    HTTP/1.1 304 Not Modified
    Last-Modified: Thu, 27 Jul 2023 04:13:31 GMT
    ETag: "64c1eeeb-e3"
    Cache-Control: max-age=240
    // 請求頭
    Cache-Control: max-age=0
    If-Modified-Since: Thu, 27 Jul 2023 04:13:31 GMT
    If-None-Match: "64c1eeeb-e3"

    似乎沒有直接使用本地緩存,默認的請求頭的max-age=0,表示不使用強緩存,但允許協商緩存,所以返回響應碼是304

    使用Chrome插件ModHeader來修改請求頭Cache-Control: max-age=240,還是304?懷疑是緩存不夠用,因為理論上而言,應該使用本地緩存

  • HTML增加外部腳本的引用,請求腳本時可以命中緩存

    試了幾次修改HTML,還是不行,給HTML增加外部的js腳本,發現腳本可以被緩存,顯示了from memory cache。可能是瀏覽器默認的策略,請求頁面的時候默認為no-cache直接進入協商緩存階段;請求其他類型資源的時候才會優先去匹配本地緩存。

    Request URL:  http://localhost:8080/cache-demo/main.js
    Status Code:  200 OK (from memory cache)

    此時設置max-age=240,發現腳本沒有按照我們預期設置的max-age過期而重新獲取新的資源,這應該是Chrome本身對資源加載的一個優化,以達到充分利用本地緩存的目的,這也解釋了有些情況下,前端代碼重新部署後,無法加載到最新內容的原因,一來HTML內容沒有更改,所以默認的協商緩存返回304,讀取了本地資源;二來HTML引用的js腳本也在本地可以找到緩存,就沒有向瀏覽器發起請求。最終導致讀取到的是舊的資源內容,需要等待幾分鐘才能讀取到新的內容。

    為了解決這個問題,可以在nginx配置顯式的聲明no-cache

    add_header 'Cache-Control' 'no-cache, max-age=240';

    這樣請求js腳本就會直接進入協商緩存,讀取到新的資源內容。

結論

根據這個測試結果,為了保證內容的時效性,建議給資源所在服務器增加no-cache的配置,並且給HTML頁面所在服務器增加no-store的配置,因為大多Vue或者react單頁應用打包構建出來的HTML頁面內容很少,往往只有一個div,配置了no-store可以保證請求到最新的資源,因為頁面內容極少,正常情況下資源返回也非常快。

user avatar qingji_58b3c385d0028 頭像 lawler61 頭像 mianduijifengba_59b206479620f 頭像
3 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.