iT邦幫忙

2021 iThome 鐵人賽

DAY 17
4

今天即將進入 Caching & Networking 章節的第一天,快取,是一個非常重要的技術,不論是前端還是後端,甚至是再更底層的系統開發,都ㄧ定會實作快取的機制來提升效能。而在前端的世界裡,又依照作用的區域分成了多種快取,今天就要來簡單介紹一下快取的觀念與前端開發裡最常遇到的一種快取 - HTTP Caching

現今的網頁架構相較於過往偏向靜態的形式已經變得複雜許多,大部分資料都要靠動態抓取,而抓取資料的過程就會產生許多 Request 請求去取得 Response ,不管是 client 端對 API 的 Ajax 操作,或是 server 端對資料庫的 query 都是類似的形式,而抓取資料的過程是需要時間的:client call API 後要等待 API response、backend 下 DB query 後也要等待資料庫查詢結果回傳,而當這樣的請求ㄧ多,例如 High Concurrency 的狀況,是很有可能對服務的性能造成影響的,為了解決這個困境,就需要這個章節的主角隆重登場了,那就是快取 (Caching)。

什麼是快取?

首先讓我們思考一下上面的問題,如果一直去發出網路請求或是 DB query 會造成性能影響的話,你想到最直覺的解決方式會是什麼?

那就不要發出網路請求或 DB query啊!?

咦,講幹話嗎,怎麼聽起來跟「吃飯會花錢怎麼辦?那就不要吃飯啊!」一樣無理。不過看似無濟於事的一個方式,卻是快取的核心概念。快取的概念其實就是提供一個額外的儲存空間,將可能需要透過請求得到的資料放在裡面,當之後要再請求資料時,先別急著發出請求,先問問快取它有沒有你要的資料吧,有的話很好,那你資料直接跟快取拿就好,也就省略了真的發出 request 的步驟,取得資料的速度也理所當然會提升,如果快取沒有你要的資料,再發出 request 去取得。而通常適合被快取的資料有兩項特性:

  • 很常被使用到
  • 資料不常變動

由上圖也可以得知使用快取的好處除了回應速度比較快之外,還有減輕網路頻寬、減緩 server 流量壓力...等等。

Cache Hit vs Cache Miss

一般在談快取時,Cache Hit 與 Cache Miss 是蠻常出現的名詞,所謂 Cache Hit 指的就是當發出請求時在快取就找到想要的資源,Cache Miss 指的就是快取中找不到想要的資源,必須再回去跟 origin server 拿資料。以效能優化的角度來說,會希望能夠盡量提高 Cache Hit 的比率,也就是盡量降低 Cache Miss 的比率,不過同時也要注意資料本身是不是過期了,假設今天一個電商網站的某個產品已經改了圖片上的價錢,但為了追求效能,盡量讓所有請求都可以 Cache Hit,使用者因此拿到舊的圖片,圖片上的價錢還是舊的,這就會產生許多問題了呢!所以根據需求讓現有快取失效並更新快取也是必須考量的問題。

快取的種類

其實快取一開始出現時是在指 OS 方面的機制,透過快取 ,CPU 可以不必一直到 main memory 去拿資料,從而減少性能的耗損,後來這個概念被運用到了 OS 層以外的地方。以 Web 開發領域來說,依照作用的層級,快取又可以被大致分 Client Cache, Server Cache(也被稱作 Application Cache) 與 Networking Cache。

在這次鐵人賽系列文中,我會專注於介紹 Client Cache 中的 HTTP Caching, Service Worker Caching 與被我歸類為 Networking Cache 的 CDN caching,例外也會提到 DNS 的 cache 機制,這些是我認為跟前端開發與效能優化比較相關且ㄧ定要知道的快取類別。

Public 與 Private 的快取

凌駕在所有快取種類之上還可以再分成公有快取與私有快取兩大類,公有快取的定義是指快取伺服器上存的回覆能給好幾個不同的請求者服務,例如請求回傳時經過的 proxy server 上的快取,這個快取上的資料可以提供給多個使用者使用。而私有伺服器相對只會服務一個請求者,例如今天主要會聚焦的瀏覽器的快取,只有在使用這個瀏覽器,也就是這台電腦的使用者可以使用快取的資源。

HTTP Caching

client cache 所指的是伺服器與瀏覽器之間的快取機制,假設今天你是一個電商平台的開發者,而你們的商品大概每過幾個月才會更換一次,而看過電商網站就知道,賣東西是需要圖片來吸引消費者的,也就是說你的網站被瀏覽時,得透過 HTTP request 去下載上百張圖片,問題在於每次瀏覽都得重新下載一次所有圖片,但剛剛也說了,這些圖片可能幾個月才會更換,重複下載相同內容是浪費效能的一件事,於是我們可以把圖片存在瀏覽器的快取中,這樣除了第一次瀏覽網站要下載外,之後就可以直接去快取取得。

從瀏覽器到伺服器的這個過程可以經過不只一層的快取,在 Caching & Networking 的章節會盡量完整的介紹每一層的快取機制,今天則會重點介紹跟 HTTP 機制相關的 HTTP Caching,也可以稱作 Browser Caching,Let's Start !

不知道各位讀者在瀏覽網頁時有沒有觀察過一個行為:通常第一次瀏覽頁面時畫面會花比較久的時間載入,而之後重新整理或重新造訪同樣頁面時載入速度會變快許多,這其實就是拜瀏覽器的快取所賜,你可以隨便開啟一個網站,重新整理後打開 Devtool 的 network tab,會看到許多資源會顯示 from disk cache,這表示這些資源來自瀏覽器的快取,並沒有再發出 request,因此載入速度會比第一次來的快。

HTTP Cache 是避免瀏覽器向伺服器發送不必要請求的一道防線,要啟用 HTTP Cache 需要伺服器端與瀏覽器端事先經過協商,至於協商的方式顧名思義就是透過 HTTP Request 來達成,瀏覽器與伺服器透過在 HTTP Request 與 Response 的 header 帶入一些資訊來協商快取的機制,例如伺服器告訴瀏覽器需不需要快取這個回傳的資源,或是判斷現在快取的資料是不是已經過期需要重新到 server 抓取...等等。

Expires

Expires 是一個舊版本的方式,伺服器在 response header 中可以加入 Expires 字段,例如:

Expires: Wed, 21 Oct 2015 07:28:00 GM

當瀏覽器收到 response 後看到 Expires 字段就會把這個資源快取起來,後續有對相同資源的請求時瀏覽器會去檢查使用者現在的時間是否有超過 Expires 中指定的過期時間,如果沒有超過,就會直接回傳快取的資料,也就是像上圖中會顯示資源是 from disk cache,如果時間超過的話就會發起 network request 跟伺服器再拿一次。

不過這會產生一個問題,因為是根據使用者電腦的時間來決定,如果使用者的電腦時間被調到了非常未來的時間點,那所有的快取都會被認定為過期,造成 Cache Miss 的狀況。

後來出現了新的 header Cache-Control 來解決這個問題,如果 response header 中同時出現 Expires 與 Cache-Control,將會採用 Cache-Control 的設定,較舊版的 Expires 將會被瀏覽器忽略。

Cache-Control

為了解決上述提到的 Expires 與一些存在於 HTTP 1.0 的快取問題,在 HTTP 1.1 時推出了 Cache-Control 這個 header。

相較於 Expires 給的是一個絕對時間,Cache-Control 通常會搭配 max-age 給一個相對時間,例如:

# 某個 http response header

Cache-Control: max-age=60

這代表使用者在收到這個 response 的 60 秒內,如果再對相同資源發出請求,就會得到快取的版本,如果超過 60 秒後才對這個資源發出請求,則會發出一個新的 network request。

而 Cache-Control 這個 header 並不是只有 max-age 可以使用,根據 MDN 文件,其實還有非常多屬性可以搭配,今天就介紹一些較常見的屬性就好 。

  • Cache-Control: no-store

    並不是所有的內容我們都希望要被快取,例如說關於使用者資訊的一些比較私密的資料或是經常變動的內容,我們會希望不要保留在 client side,而是每次都到 server 去抓取,這時候可以在 response header 中加入

    Cache-Control: no-store
    ```
    這個字段告訴瀏覽器「請不要幫我對這個資源做任何快取」。
    
  • Cache-Control: no-cahce

    這個字段等介紹完下面的其他特性後我再回過頭來補充。目前各位要記住,雖然它叫做 no-cache,但它代表的並不是告訴瀏覽器不要做任何快取。

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

  • Cache-Control: private

    代表此 response 只可以被瀏覽器儲存起來。

  • Cache-Control: public

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

  • Cache-Control: s-maxage

    s-maxage 會覆寫 max-age 或者 Expires 標頭,不過只對共用快取軟體生效(比如 nginx)。私有快取(Browser)會無視這個指令。

快取時效過期後可以做些什麼?

既然設定了快取的時效,那麼接下來就要討論當快取過期了之後,能夠做些什麼。也許你的第一個反應就是再回到伺服器拿一次資料就好,但萬一檔案並沒有任何改變呢?例如一個企業的官網總會有 logo,但 logo 也許過了一年也是沒有任何更改的,這樣去重新抓取似乎有點沒有效率且影響效能,接下來就來看看快取過期後,我們能不能透過一些方式,盡量有效運用原有的快取。

Last-Modified & If-Modified-Since

首先先來介紹從 HTTP 1.0 就被提出的比較舊的解決方案:Last-Modified 搭配 If-Modified-Since。

為了怕第一次接觸的讀者搞混,這邊先確認一下它們各自是出現在哪裡:

  • Last-Modified: 出現在伺服器回應給瀏覽器的 response header 裡,告訴瀏覽器這個檔案上次更改是什麼時候。
  • If-Modified-Since: 出現在瀏覽器發出請求的 resquest header 裡,用來跟伺服器確認檔案在某個時間點後是不是有經過更改。

而它們的使用流程是這樣的,舉例來說,當你對公司形象網站的 Logo 第一次發出網路請求時,伺服器的 response header 可能會帶入以下資訊

Last-Modified: Sat, 11 Sep 2021 07:28:00 GMT
Cache-Control: max-age=31536000 # 一年

瀏覽器收到 response 後會把 Logo 快取起來,設定過期時限是一年,並記錄檔案最後修改日期是 Sat, 11 Sep 2021 07:28:00。

在這一年內,每當有對這個 Logo 的請求時,瀏覽器就會回傳快取的版本,而當超過一年,原本快取的時效過期時,瀏覽器就會帶上之前紀錄的更改時間,去問伺服器從這個時間點以後,這個檔案是否有被更改過。

# HTTP request header

GET /ironman-logo.png
If-Modified-Since: Sat, 11 Sep 2021 07:28:00 GMT

假設伺服器認定檔案被更改過,那就會回傳一份新的檔案給瀏覽器,之後的快取流程就會按照前面提過的重新跑一次。但是如果伺服器查詢了一下發現這個檔案從瀏覽器提供的時間之後就都沒有修改過,就會告訴使用者「你可以繼續使用快取的版本」,伺服器會回傳一個 304 的 Status code,304 代表 Not Modified,瀏覽器收到 304 後就會知道可以繼續沿用之前的快取版本。

不過 Last-Modified & If-Modified-Since 的方法會產生一個問題,就是它是根據檔案的編輯時間來做判斷的,如果今天檔案只是被打開便重新存檔,內容完全沒有更動,因為編輯時間改動,伺服器會認為這個檔案被改變。更好的做法應該是依據檔案的內容有沒有更動來決定是否要重新抓取檔案,接下來要講的 Etag & If-None-Match 就解決了這個問題。

Etag & If-None-Match

不知道大家是不是都有聽過 JWT token 這個身份驗證機制,它的概念就是利用雜湊函數去判斷 token 有沒有經過人為改變來判斷是不是合法的 token。

Etag 也是利用類似的概念,相同內容的檔案會產生獨一無二的 etag 值,就算是加一個空白或標點符號也會導致 Etag 改變。

使用 Etag 的流程變成 server 在 response header 會帶入 etag 讓瀏覽器存起來

Cache-Control: max-age=86400
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

等到快取時效過後,使用者又請求了相同資源,瀏覽器就會在 request header 中帶入:

If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

伺服器會去檢驗瀏覽器帶過來的 Etag 與最新的檔案是否相符,如果相同,代表檔案內容沒有變,瀏覽器可以繼續使用相同的快取,如果不符合,代表瀏覽器快取中已是舊的版本,因此需要重新抓取一次。

避免 Mid-Air Collisions

有時候在頁面的編輯模式時,例如修改表單資訊(這邊指的不是可以同時共編的系統),會想要避免同時有多人編輯的狀況,避免資料的不一致性。這時候 Etag 也可以幫上忙。

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

ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

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

If-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

如果伺服器發現 hash 值不一致,代表當你在編輯的時候,有人同時也編輯了內容,這時候伺服器可以回傳 412 的 status code,412 代表 Precondition Failed,告訴使用者需要重新再編輯一次。

最後自己畫了一張簡單的圖表統整一下今天提到的 HTTP Caching 的運作流程

要注意如果快取如果沒過期的話,直接從快取拿是不會發出網路請求的,如果是使用 Etag 或是 If-Modified-Since 的機制的話,實際上還是會發出網路請求到 server,只是網路封包大小通常會比真的跟伺服器抓取資料還要小許多,不過因為還是發出了網路請求,效能一定還是比直接使用快取還要差,因此選擇適合的 cache policy 是非常重要的喔!

Cache Busting

看完上面的內容,聰明的你可能會想到一些問題:「如果今天檔案被更改了,例如網站的 background-color 原本是白色,後來修正成黑色,但因為瀏覽器的快取還沒有過期,會繼續載入舊的 CSS file,使用者有可能一直從快取拿到舊的版本,也就是說使用者看到的網站背景顏色一直會是白色的,直到快取到期才會更新。

這樣不太對啊...也就是說應該要有一種機制可以在檔案更新時讓瀏覽器知道這件事,而不再使用舊的快取。Cache Busting 就是一種為檔案建立一個獨立識別檔名,只要檔名變了,瀏覽器就認為這是一個新的檔案,需要重新跟伺服器抓取,其實這就有點像把前面介紹的 Etag 機制實作在 HTML file 裡面而已,例如這樣子:

<!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>

不過如果每次都要自己改 JS 或是 CSS 的檔名,同時也要修改 HTML 裡面引入的檔名也太麻煩了吧?

不要擔心,像 webpack 這些 bundler 都可以做到在打包時加一串 hash 字串到檔名裡,並同時自動更新 HTML 裡面引入的檔名,以上面修改 CSS background color 的例子來說,修正後重新打包的 HTML 可能會變成這個樣子

<!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 檔名中的 hash 有改變。
再將 HTML 檔案設成 Cache-Control: no-cache,每一次請求時都會去重新檢視一次 HTML 是否改變,上面的例子因為引入的 CSS 檔名變了,瀏覽器會認定需要重新抓取一次檔案,就不會使用過期的快取。這樣的機制其實簡單來說就是讓每一個版本的檔案都有一個獨一無二的檔名,當檔案內容改變,就更新檔名,強制瀏覽器重新抓取一次。

Memory Cache vs Disk Cache

如果你是 Chrome 瀏覽器的愛好者,可能曾經發現除了 from disk cache 以外,偶爾還會出現一些資源是顯示 from memory cache,memory cache 又是什麼呢?

memory cahce 並不是所有瀏覽器都會實作的一種快取,這邊可以看作是 Chrome 特別實作的一種快取(其他瀏覽器我就不清楚了),顧名思義它把資源存在 memory (RAM) 裡面,所以在效能上會比 disk cache 還要快,但缺點是存在裡面的資料具有揮發性,當關閉瀏覽器時,原本存在 memory 的資料就會被清空。

至於瀏覽器怎麼決定哪些資源要放到 memory cache 裡,目前沒有明確的定義,只知道透過 resource hint 例如 preload, prefetch 載入的資源比較有機會被放到 memory cache 中。你可能會覺得,既然速度那麼快,就把全部資源都先存到 memory cache 就好啦?這是不可能的,記憶體的容量相比硬碟小非常多,如何有效運用 memory 的空間是很難的問題,所幸這塊身為開發者的我們不必擔心,瀏覽器都幫我們做好了。

需要特別注意的是,memory cache 在快取資源時不會管 HTTP header Cache-Control 的設定,同時在識別時也不像前面介紹的 HTTP Caching 是用 URL 或檔名來判斷,而會另外判斷 Content-Type, CORS 等其他特徵。

最後提一下 memory cache 相比其他 cache 的優先順序,在明天我們將介紹 service worker 的 cache,這邊讀者可以先記一下三種快取的優先順序(指會先到哪種 cache 找有沒有資料)

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

本日小結

Caching & Networking 章節,總共會介紹 4 種快取,今天介紹的 HTTP Caching (又被稱作 browser caching),是四種快取中最基本且最被廣泛使用的一種。網路上有非常多的文章在討論使用快取前後對網站效能的影響,如果還不明白快取的強大,建議可以去參考一下。

快取對我來說迷人的地方就是它僅僅是一個概念,但是從應用層到系統底層都可以看到它的蹤跡,並且在不同地方會有獨特的實作方式,這次系列文也僅僅會提到 client side 的 cache,也就只是快取機制的冰山一角,聽到這裡,你是不是也對於後續幾天會學到的快取機制感到興奮了呢?

Reference & 圖片來源

https://www.keycdn.com/support/web-cache

https://wp-rocket.me/blog/cache-miss-vs-cache-hit/#section-2

https://blog.techbridge.cc/2017/06/17/cache-introduction/

https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Caching


上一篇
Day16 X Polyfill-less Bundling Script & File Compression
下一篇
Day18 X Service Workers Cache
系列文
今晚,我想來點 Web 前端效能優化大補帖!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言