面試精選之http緩存

前端面試常問第二大問題是http緩存相關內容。說真的,http緩存相關的細節比較多,而且 http 經常使用協議版本有1.0、1.1,(本文暫不討論http2.0)。javascript

緩存相關 header

咱們先羅列一下和緩存相關的請求響應頭。css

  • Expires

響應頭,表明該資源的過時時間。html

  • Cache-Control

請求/響應頭,緩存控制字段,精確控制緩存策略。前端

  • If-Modified-Since

請求頭,資源最近修改時間,由瀏覽器告訴服務器。java

  • Last-Modified

響應頭,資源最近修改時間,由服務器告訴瀏覽器。webpack

  • Etag

響應頭,資源標識,由服務器告訴瀏覽器。web

  • If-None-Match

請求頭,緩存資源標識,由瀏覽器告訴服務器。面試

配對使用的字段:瀏覽器

  • If-Modified-Since 和 Last-Modified
  • Etag 和 If-None-Match

今天着重介紹一下瀏覽器緩存機制,咱們知道,瀏覽器緩存通常都是針對靜態資源,好比 js、css、圖片 等,因此咱們下面的例子圍繞一個 javascript 文件 a.js 來闡述。拋開理論式灌輸,咱們從實際場景觸發,一點點完善緩存機制,這種方式,相信你們會更容易理解。緩存

作一些約定,方便之後比較。

  • a.js 大小爲 10 KB
  • 請求頭約定爲 1 KB
  • 響應頭約定爲 1 KB

原始模型

  • 瀏覽器請求靜態資源 a.js。(請求頭:1KB)
  • 服務器讀取磁盤文件 a.js,返給瀏覽器。(10KB(a.js)+1KB(響應頭) = 11KB)。
  • 瀏覽器再次請求,服務器又從新讀取磁盤文件 a.js,返給瀏覽器。
  • 如此循環。。

執行一個往返,流量爲 10(a.js)+1(請求頭)+1(響應頭) = 12KB。

訪問 10 次,流量大約爲12 * 10 = 120KB。

因此,流量與訪問次數有關:

L(流量) = N(訪問次數) * 12。

該方式缺點很明顯:

  • 浪費用戶流量。
  • 浪費服務器資源,服務器要讀磁盤文件,而後發送文件到瀏覽器。
  • 瀏覽器要等待 a.js 下載而且執行後才能渲染頁面,影響用戶體驗。

js 執行時間相比下載時間要快的多,若是能優化下載時間,用戶體驗會提高不少。

瀏覽器增長緩存機制

  • 瀏覽器第一次請求 a.js,緩存 a.js 到本地磁盤。(1+10+1 =12KB)
  • 瀏覽器再次請求 a.js,直接走瀏覽器緩存(200,from cache),再也不向服務器發起請求。(0KB)
  • ...

第一次訪問,流量爲 1+10+1 = 12KB。 第二次訪問,流量爲 0。 。。。 第 10000 次訪問,流量依然爲 0。

因此流量與訪問次數無關:

L(流量) = 12KB。

優勢:

  • 大大減小帶寬。
  • 因爲減小了 a.js 下載時間,相應的提升了用戶體驗。

缺點:服務器上 a.js 更新時,瀏覽器感知不到,拿不到最新的 js 資源。

服務器和瀏覽器約定資源過時時間。

服務器和瀏覽器約定文件過時時間,用 Expires 字段來控制,時間是 GMT 格式的標準時間,如 Fri, 01 Jan 1990 00:00:00 GMT。

  • 瀏覽器第一次請求一個靜態資源 a.js。(1KB)
  • 服務器把 a.js 和 a.js 的緩存過時時間(Expires:Mon, 26 Sep 2018 05:00:00 GMT)發給瀏覽器。(10+1=11KB)

服務器告訴瀏覽器:你把我發給你的 a.js 文件緩存到你那裏,在 2018年9月26日5點以前不要再發請求煩我,直接使用你本身緩存的 a.js 就好了。

  • 瀏覽器接收到 a.js,同時記住了過時時間。
  • 在2018年9月26日5點以前,瀏覽器再次請求 a.js,便再也不請求服務器,直接使用上一次緩存的 a.js 文件。(0KB)
  • 在2018年9月26日5點01分,瀏覽器請求 a.js,發現 a.js 緩存時間過了,因而再也不使用本地緩存,而是請求服務器,服務器又從新讀取磁盤文件 a.js,返給瀏覽器,同時告訴瀏覽器一個新的過時時間。(1+10+1=12KB)。
  • 如此往復。。。

該種方式較以前的方式有了很大的改善:

  • 在過時時間之內,爲用戶省了不少流量。
  • 減小了服務器重複讀取磁盤文件的壓力。
  • 緩存過時後,可以獲得最新的 a.js 文件。

缺點仍是有:

  • 緩存過時之後,服務器無論 a.js有沒有變化,都會再次讀取 a.js文件,並返給瀏覽器。

服務器告訴瀏覽器資源上次修改時間。

爲了解決上個方案的問題,服務器和瀏覽器通過磋商,制定了一種方案,服務器每次返回 a.js 的時候,還要告訴瀏覽器 a.js 在服務器上的最近修改時間 Last-Modified (GMT標準格式)。

  • 瀏覽器訪問 a.js 文件。(1KB)

  • 服務器返回 a.js 的時候,告訴瀏覽器 a.js 文件。(10+1=11KB) 在服務器的上次修改時間 Last-Modified(GMT標準格式)以及緩存過時時間 Expires(GMT標準格式)

  • 當 a.js 過時時,瀏覽器帶上 If-Modified-Since(等於上一次請求的Last-Modified) 請求服務器。(1KB)

  • 服務器比較請求頭裏的 Last-Modified 時間和服務器上 a.js的上次修改時間:

    • 若是一致,則告訴瀏覽器:你能夠繼續用本地緩存(304)。此時,服務器再也不返回 a.js 文件。(1KB)
    • 若是不一致,服務器讀取磁盤上的 a.js 文件返給瀏覽器,同時告訴瀏覽器 a.js 的最近的修改時間 Last-Modified 以及過時時間 Expires。(1+10=11KB)
    • 如此往復。

此種方案比上一個方案有了更進一步的優化:

  • 緩存過時後,服務器檢測若是文件沒變化,再也不把a.js發給瀏覽器,省去了 10KB 的流量。
  • 緩存過時後,服務器檢測文件有變化,則把最新的 a.js 發給瀏覽器,瀏覽器可以獲得最新的 a.js。

缺點:

  • Expires 過時控制不穩定,由於瀏覽器端能夠隨意修改時間,致使緩存使用不精準。
  • Last-Modified 過時時間只能精確到秒。

精確到秒存在兩個問題:

  • 一、若是 a.js 在一秒時間內常常變更,同時服務器給 a.js 設置無緩存,那瀏覽器每次訪問 a.js,都會請求服務器,此時服務器比較發給瀏覽器的上次修改時間和 a.js 的最近修改時間,發現都是在同一時間(由於精確到秒),所以返回給瀏覽器繼續使用本地緩存的消息(304),但事實上服務器上的 a.js 已經改動了好屢次了。因此這種狀況,瀏覽器拿不到最新的 a.js 文件。
  • 二、若是在服務器上 a.js 被修改了,但其實際內容根本沒發生改變,會由於 Last-Modified 時間匹配不上而從新返回 a.js 給瀏覽器。

繼續改進,增長相對時間的控制,引入 Cache-Contorl

爲了兼容已經實現了上述方案的瀏覽器,同時加入新的緩存方案,服務器除了告訴瀏覽器 Expires ,同時告訴瀏覽器一個相對時間 Cache-Control:max-age=10秒。意思是在10秒之內,使用緩存到瀏覽器的 a.js 資源。

瀏覽器先檢查 Cache-Control,若是有,則以 Cache-Control 爲準,忽略 Expires。若是沒有 Cache-Control,則以 Expires 爲準。

繼續改進,增長文件內容對比,引入Etag

爲了解決文件修改時間只能精確到秒帶來的問題,咱們給服務器引入 Etag 響應頭,a.js 內容變了,Etag 才變。內容不變,Etag 不變,能夠理解爲 Etag 是文件內容的惟一 ID。 同時引入對應的請求頭 If-None-Match,每次瀏覽器請求服務器的時候,都帶上If-None-Match字段,該字段的值就是上次請求 a.js 時,服務器返回給瀏覽器的 Etag。

  • 瀏覽器請求 a.js。
  • 服務器返回 a.js,同時告訴瀏覽器過時絕對時間(Expires)以及相對時間(Cache-Control:max-age=10),以及a.js上次修改時間Last-Modified,以及 a.js 的Etag。
  • 10秒內瀏覽器再次請求 a.js,再也不請求服務器,直接使用本地緩存。
  • 11秒時,瀏覽器再次請求 a.js,請求服務器,帶上上次修改時間 If-Modified-Since 和上次的 Etag 值 If-None-Match。
  • 服務器收到瀏覽器的If-Modified-Since和Etag,發現有If-None-Match,則比較 If-None-Match 和 a.js 的 Etag 值,忽略If-Modified-Since的比較。
  • a.js 文件內容沒變化,則Etag和If-None-Match 一致,服務器告訴瀏覽器繼續使用本地緩存(304)。
  • 如此往復。

結束了嗎?

到此就結束了嗎? 是的,http的緩存機制就是如此了,可是仍然存在一個問題:

瀏覽器沒法主動得知服務器上的 a.js 資源變化了。

無論用 Expires 仍是 Cache-Control,他們都只可以控制緩存是否過時,可是在緩存過時以前,瀏覽器是沒法得知服務器上的資源是否變化的。只有當緩存過時後,瀏覽器纔會發請求詢問服務器。

最終方案

你們能夠想象咱們使用 a.js 的場景,咱們通常都是輸入網址,訪問一個 html 文件,html文件中會引入 js、css 、圖片等資源。

因此呢,咱們在html上作些手腳。

咱們不讓 html 文件緩存,每次訪問 html 都去請求服務器。因此瀏覽器每次都能拿到最新的html資源。

a.js 內容更新的時候,咱們修改一下 html 中 a.js 的版本號。

  • 第一次訪問 html
<script src="http://test.com/a.js?version=0.0.1"></script>
複製代碼
  • 瀏覽器下載0.0.1版本的a.js文件。

  • 瀏覽器再次訪問 html,發現仍是0.0.1版本的a.js文件,則使用本地緩存。

  • 某一天a.js變了,咱們的html文件也相應變化以下:

<script src="http://test.com/a.js?version=0.0.2"></script>
複製代碼

因此,經過設置html不緩存,html引用資源內容變化則改變資源路徑的方式,就解決了沒法及時得知資源更新的問題。

固然除了以版本號來區分,也能夠以 MD5hash 值來區分。 如

<script src="http://test.com/a.【hash值】.js"></script>
複製代碼

使用webpack打包的話,藉助插件能夠很方便的處理。

除此之外的東東

Cache-Control 除了能夠設置 max-age 相對過時時間之外,還能夠設置成以下幾種值:

  • public,資源容許被中間服務器緩存。

瀏覽器請求服務器時,若是緩存時間沒到,中間服務器直接返回給瀏覽器內容,而沒必要請求源服務器。

  • private,資源不容許被中間代理服務器緩存。

瀏覽器請求服務器時,中間服務器都要把瀏覽器的請求透傳給服務器。

  • no-cache,瀏覽器不作緩存檢查。

每次訪問資源,瀏覽器都要向服務器詢問,若是文件沒變化,服務器只告訴瀏覽器繼續使用緩存(304)。

  • no-store,瀏覽器和中間代理服務器都不能緩存資源。

每次訪問資源,瀏覽器都必須請求服務器,而且,服務器不去檢查文件是否變化,而是直接返回完整的資源。

  • must-revalidate,能夠緩存,可是使用以前必須先向源服務器確認。
  • proxy-revalidate,要求緩存服務器針對緩存資源向源服務器進行確認。
  • s-maxage:緩存服務器對資源緩存的最大時間。

Cache-Control 對緩存的控制粒度更細,包括緩存代理服務器的緩存控制。

文章介紹到此,若有興趣,能夠動手實踐下。

相關文章
相關標籤/搜索