Try   HackMD

前端效能優化 Day17 X 初探快取 & HTTP Caching

本章節重點 : Server 跟瀏覽器之間的 Cache 機制 => HTTP Caching

什麼是 Cache?

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

  • 提供一個暫存可能使用到資料的額外儲存空間
  • Cache 是避免 client 和 sever 提出請求的一種方式
  • Response 的再利用性(Reusability): 瀏覽器可以在一段時間內重複使用已經快取的回應,而不需要再次向伺服器發送請求

適合被快取的資料

  1. 很常被使用到
  2. 資料不常變動

為什麼需要 Cache

節省流量,節省時間,減少資源的損耗

Cache Hit vs Cache Miss

  • Cache Hit : 指的就是當發出請求時在快取就找到想要的資源
  • Cache Miss : 指的就是快取中找不到想要的資源,必須再回去跟 origin server 拿資料
  • 效能優化的角度 : 提高 Cache Hit Ratio,降低 Cache Miss Ratio

快取的種類

  • 起源: 應用於 OS 方面的機制,透過快取,CPU 可以不必一直到 main memory 去拿資料,後來這個概念被運用到了 OS 層以外的地方。

  • 分類: 以 Web 開發領域來說,依照作用的層級

    1. Client Cache : HTTP Caching, Service Worker Caching
    2. Server Cache(Application Cache): Redis
    3. Networking Cache : CDN caching

Public 與 Private 的快取

  • 這個 response 能不能被不同 users 共享
    image
  • 公有快取 : 指快取伺服器上存的回覆能給好幾個不同的請求者服務。
  • 私有快取 : 相對只會服務一個請求者,這台電腦的使用者可以使用快取的資源。

HTTP Caching

image

  • HTTP Cache 是避免瀏覽器向伺服器發送不必要請求的一道防線
  • 要啟用 HTTP Cache 需要伺服器端與瀏覽器端事先經過協商,瀏覽器與伺服器透過在 HTTP Request 與 Response 的 header 帶入一些資訊來協商快取的機制

Expires

  • HTTP 1.0 舊版本的方式,伺服器在 response header 中可以加入 Expires 字段

    ​​​​Expires: Wed, 21 Oct 2015 07:28:00 GM
    
  • 機制 : 取決於瀏覽器「現在的時間」是否有超過 Expires 中指定的過期時間

  • 結果 :

    • 沒有超過 =>瀏覽器「不會發送任何 Request」直接從的 Cache 拿資料
    • 超過 => 發起 network request

    image

    「Status code 200 (from disk cache)」,代表這個 Request 其實沒有發出去,Response 是直接從 disk cache 裡面拿的。

  • 問題 : 根據使用者電腦本身的時間來決定過期與否 => Cache Miss

Cache-Control 與 max-age

  • 為解決 Expires 的問題

  • HTTP 1.1 時推出了 Cache-Control 這個 header

    ​​​​# 某個 http response header
    ​​​​Cache-Control: max-age=60
    
  • 機制 : 使用者在收到這個 response 的 60 秒內,如果再對相同資源發出請求,就會得到快取的版本,如果超過 60 秒後才對這個資源發出請求,則會發出一個新的 network request。

  • 範例: 觀察 Google Logo 檔案的 Response header

    image

    • max-age 設定成 31536000 秒(365 天)
    • 在一年之內造訪這個網站,都不會對 Google logo 這張圖片送出 Request,而是會直接使用快取 : Status code 200 (from memory cache)

Expires跟max-age優先性?

根據RFC2616的定義:

If a response includes both an Expires header and a max-age directive, the max-age directive overrides the Expires header, even if the Expires header is more restrictive

max-age會蓋過Expires。因此現在的快取儘管兩個都會放,但其實真正會用到的是max-age。

tips :

  1. Expires 和 Cache-Control 這兩個 Header 都是在關注一個 Response 的「新鮮度(freshness)」
  2. Expires 給定絕對時間,Cache-Control 通常會搭配 max-age 給定相對時間

過期了,然後呢?
「過期了不代表不能用」

Last-Modified & If-Modified-Since

  • HTTP 1.0 就被提出的比較舊的解決方案
  • Last-Modified : 出現在伺服器回應給瀏覽器的 response header 裡,告訴瀏覽器這個檔案上次更改是什麼時候。
  • If-Modified-Since : 出現在瀏覽器發出請求的 resquest header 裡,用來跟伺服器確認檔案在某個時間點後是不是有經過更改。
Last-Modified: 2017-01-01 13:00:00 // 檔案的最後更新時間
Cache-Control: max-age=31536000 // 過期時間
  • 機制:

    • 半年後重新請求這張圖片 => 不會發送任何 Request,而是直接從瀏覽器那邊獲得資料。

    • 一年之後再請求一次這張圖片 => 瀏覽器和 Server 確認檔案從2017-01-01 13:00:00以後有沒有更新」,會發送出下面這樣的 Request:

      ​​​​​​​​GET /logo.png
      ​​​​​​​​If-Modified-Since: 2017-01-01 13:00:00
      
    1. 檔案更新 => 瀏覽器會收到一份新的檔案。

    2. 檔案未更新 => Server 回一個Status code : 304 (Not Modified),代表可以繼續沿用快取的這份檔案。

      image

  • 問題 : 根據檔案的編輯時間來做判斷的

Etag & If-None-Match

  • 為解決 Last-Modified & If-Modified-Since 的問題

  • 機制 : 依據檔案的內容有沒有更動來決定是否要重新抓取檔案

  • 流程 :

    • server 在 response header 會帶入 etag 讓瀏覽器存起來
    ​​​ Cache-Control: max-age=86400
    ​​​ ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
    
    • 等到快取時效過後,使用者又請求了相同資源,瀏覽器就會在 request header 中帶入:
    ​​​​If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
    
    • 比對瀏覽器帶過來的 Etag 與最新的檔案是否相符
      • 相同 => 代表檔案內容沒有變,瀏覽器可以沿用相同的快取
      • 不同 => 代表瀏覽器快取中已是舊的版本,需要重新抓取一次
  • (補充) 生成 etag 的原理

    • 對於靜態文件(例如 HTML、CSS、JavaScript、圖像等),通常會使用文件的 size 加 mtime (修改時間)。
    • 對於動態生成的內容,例如字符串或緩衝區(Buffer),通常會使用字串或者 Buffer 的長度加上透過 sha1 演算法產生的 hash 字串的前27位。

(補充) 避免 Mid-Air Collisions

  • 目標:使用 ETag 避免多人同時編輯的情況

  • 流程:當從 view mode 進到 edit mode 時,可以先紀錄當前頁面的 Etag 值

    ​​​​ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
    

    等到修改完成時透過 If-Match 這個 header 檢查 Etag 是否一樣

    ​​​​If-Match: "33a64df551425fcc55e4d42a148795d9f25f89d5"
    

    ETag 值不一致,表示發生共編 => 伺服器回應 412 狀態碼,代表前置條件失敗(Precondition Failed),告知使用者需要重新編輯

tips:

image

  • 選擇適合的 cache policy 是非常重要
    • 快取沒過期 : 直接拿不會發出網路請求
    • 使用 Etag 或是 If-Modified-Since 的機制 : 會發出網路請求,只是網路封包大小通常會比真的跟伺服器抓取資料還要小許多

不要快取怎麼辦?

含有一些機密資料的頁面

Cache-Control: no-store
代表不要任何快取

Cache-Control: no-cahce
代表此 response 必須經過驗證才能使用

不要任何快取是 no-store 不是 no-cahce!

Cache-Control: private
代表此 response 只可以被瀏覽器儲存起來

Cache-Control: public
代表此 response 可以被任何快取軟體儲存起來,例如 reverse proxy 的快取、瀏覽器的快取

no-cahce 的應用 - 首頁的快取策略

  • 目標:「把頁面快取起來,但只要首頁一變動,就能夠立刻看到新的頁面」

    [解法1] Cache-Control: max-age=0 (代表這個 Response 0 秒之後就會過期) 再搭配 Etag 來使用 :
    第一個 Response:

    ​​​​Cache-Control: max-age=0
    ​​​​Etag: 1234
    

    重新整理,瀏覽器發出 Request:

    ​​​​If-None-Match: 1234
    
    • 檔案沒有變動,無需發出 Request,Server 回傳:304 Modified,沿用舊檔案
    • 檔案有變動,回傳新的檔案並更新 Etag

    [解法2] Cache-Control: no-cache :

    • 無論檔案有沒有變動,每次都會發送 Request 去確認

max-age=0 允許快取在重新驗證後使用舊的回應,而 no-cache 則要求快取必須重新驗證回應,確保每次都使用最新的數據。

Cache-Control: no-store v.s. Cache-Control: no-cahce

  • 案例 : A 網站是使用 Cache-Control: no-store,B 網站是使用 Cache-Control: no-cache
  • 假設 index.html 有 100 kb
  • 假設封包大小 1kb
  • 結果:
    • 造訪了十次 A,累積的流量就是 1000kb
    • 造訪了十次 B,但 B 第十次才更新,累積流量就是 9 (前九次 Server 只會回傳 Status code 304) + 100 = 109 kb

達成的目標一致 : 只要網站更新,使用者就能立即看到結果,但是 B 的流量遠低於 A,因為有善用快取策略。

tips:

  • max-age=0 跟 no-cache 的差異,SHOULD-revalidate 和 MUST-revalidate
  • no-store 跟 no-cache 的差異,永遠不用快取跟永遠檢查快取

Cache Busting

no-cache 無論如何,都會發出 Request。有沒有可能,連 Request 都不發呢?

  • 目標:檔案不更新,瀏覽器就不會發 Request,直接沿用快取。檔案一更新,瀏覽器就要立即抓取新的檔案
  • 機制: Cache Busting - 為檔案建立一個獨立識別檔名,只要檔名變了,瀏覽器就認為這是一個新的檔案,需要重新跟伺服器抓取(類似把 Etag 機制實作在 HTML file裡面)
    • webpack 這些 bundler 都可以做到在打包時加一串 hash 字串到檔名裡,並同時自動更新 HTML 裡面引入的檔名
<!DOCTYPE html> <html> <head> <link rel='stylesheet' href='./css/public/style-dasd2134das.css'></link> <script src='./public/js/ajskdj1213.js'></script> </head> <body> <div id="container"> <!-- 交給 SPA 去 render --> </div> </body> </html>
  • 以下方修改 CSS 後重新打包的 HTML 為例 : 只有 CSS 檔名中的 hash 有改變
<!DOCTYPE html> <html> <head> <link rel='stylesheet' href='./css/public/style-eawe124as.css'></link> <script src='./public/js/ajskdj1213.js'></script> </head> <body> <div id="container"> <!-- 交給 SPA 去 render --> </div> </body> </html>
  • 原理: 針對不同的檔案採用不同的快取策略,並且直接用「更換 CSS 或 JavaScript 檔案」的方式強制瀏覽器重新下載
  • 策略:
    對於 CSS 檔案:快取策略為 max-age=31536000
    對於 index.html 檔案:快取策略為 no-cache
    所以每一次訪問這個頁面,都會去看 index.html 是否更新,因為引入的 CSS 檔名變了,重新發送 Request

Memory Cache vs Disk Cache

image

  • from disk cache : 表示這些資源來自瀏覽器的快取,並沒有再發出 request,因此載入速度會比第一次來的快。
  • from memory cahce : 可以看作是 Chrome 特別實作的一種快取,它把資源存在 memory (RAM) 裡面,所以在效能上會比 disk cache 還要快,缺點是關閉瀏覽器時資料就會被清空。
    • 目前沒有明確的定義瀏覽器怎麼決定哪些資源要放到 memory cache 裡,只知道透過 resource hint 例如 preload, prefetch 載入的資源比較有機會被放到 memory cache 中。

memory cahce v.s. disk cache

  • 效能與準確性 : memory cahce > disk cache
  1. 不受 Cache-Control 設定影響:即使資源的 Cache-Control 指令要求不快取,memory cache 也可能會將其存儲起來以提高後續的訪問速度
  2. 識別特徵不同: 傳統的 HTTP 快取通常根據 URL 或檔案名稱來識別和存儲資源,但 memory cache 會額外考慮資源的 Content-Type 和 CORS 等其他特徵來識別和區分資源

既然速度那麼快,就把全部資源都先存到 memory cache 就好啦?
這是不可能的,記憶體的容量相比硬碟小非常多

(補充) cache 的優先順序(指會先到哪種 cache 找有沒有資料)

memory cache -> service worker cache -> disk cache (browser cache)

結語

  • Expires 跟 max-age 是在負責看這個快取是不是「新鮮」
  • Last-Modified, If-Modified-Since, Etag, If-None-Match 是負責詢問這個快取能不能「繼續使用」
  • no-cache 與 no-store 代表到底要不要使用快取,以及應該如何使用
  • 所有透過瀏覽器發送的 HTTP 請求,都會先經過瀏覽器的快取,在這裡會先檢查是否有有效的快取內容可作為回應,如果有的話就直接讀取快取的內容,以減少網路的延遲和傳輸造成的成本

參考文章

https://ithelp.ithome.com.tw/articles/10276125