# Cache and preload ## Sharing Time 2023/02/07 16:00~17:00 ## Reference - [web] 內容 Image 預先或延遲載入(preload, lazy load, prefetch) : https://pjchender.dev/webdev/web-preload-lazyload/ - Preload vs Prefetch : https://www.cythilya.tw/2018/07/31/preload-vs-prefetch/ - 優先度、prefetch、preload and preconnect https://shubo.io/preload-prefetch-preconnect/ - Recommended reading - preload-prefetch-and-priorities-in-chrome : https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf - Current Chrome Priority setting: https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/ - Cache: https://calendar.perfplanet.com/2016/a-tale-of-four-caches/ https://oldmo860617.medium.com/%E6%A5%B5%E9%99%90%E5%8A%A0%E9%80%9F-web-%E9%96%8B%E7%99%BC%E8%80%85%E4%B8%8D%E8%83%BD%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84-cache-%E5%A4%A7%E8%A3%9C%E5%B8%96-3c7a9c4241de ## 問題背景與決定 面對的問題: - 由於圖片的延遲載入,讓我們的頁面看起來有一段反應的載入時間,我們要如何解決? - Ans: - 直接嵌入 ? 無須考量到優先程度的問題,因為他會在 html 被載入的時候一起出現,之後才是載入 css,所以符合先載入 svg 再載入 css 的原則 - preload ? 需要考量到資料是否會在 css 載入完畢前載入完成,這個部分就要考慮到 Server 的 http cache 相關的 header 設定 最終的考量與取捨: - 經過考慮可能可以使用 preload,但是最後卻放棄使用 preload 的原因是什麼? - Ans: preload 受 cache (http cache & memory cache) 所影響,而瀏覽器的 memory cache 是從 RAM 裡面拿出資料的,雖然設定 preload 讓資源有可能會在沒辦法在 http-cache 存取的情況下,最終進入到 memory cache,讓我們優先拿到資料,但那終究需要考量 RAM 使否是在一個"瀏覽器認為 RAM 使用率比較低,且可以放進 RAM 的情況下"才會實現 --- ## 目錄 - 優先程度 Priority - 改變資源優先程度的方式 - Cache - Chrome 的四大 Cache - Preload 、 Prefetch 、 Preconnect ## 優先程度 Priority - Priority 代表著什麼? 而 Priority 受什麼影響 ? > **The resource download sequence depends on the browser's assigned priority** for every resource on the page. Different factors can affect priority computation logic. - Priority 代表著 resource 被 download 的優先順序 - Priority 優先順序例子 (請看同心圓的符號,此處的 fetchpriority 是指用來調整相對優先序的設定): ![圖一](https://i.imgur.com/otiYX9c.png) ![圖一](https://i.imgur.com/A1m7wrU.png) ![圖一](https://i.imgur.com/az1K8pS.png) 說明: > Chromium loads resources in 2 phases. “Tight mode” is the initial phase and constrains loading lower-priority resources until the body is attached to the document (essentially, after all blocking scripts in the head have been executed). In tight mode, low priority resources are only loaded if there are less than 2 in-flight requests at the time that they are discovered. - 在 Chrome 中,load resource 有兩個階段: "是 tight mode" 和 "非 tight mode" - tight mode: 一開始 load resource 的階段,基本上 load 資源的順序會先排除掉 low priority 的 resource,但在此時如果發現了 low priority 的資源,瀏覽器會比較在發現時是否同時存在小於兩個正在加載的資源,如果有符合這個情形,則允許加載優先程度較低的資源,一直到 body 被加入到 document 才開始加入其他剩餘的資源。 - 白話文解釋: 我們在 dev tool 看到的那些 network 上 load 資源的 priority,並不是絕對的下載資料的順序,他實際的順序會經過 Chrome 調整,也就是說按照 Chrome 的 tight mode 規則,會先盡"可能"的把有優先權較低的資源先擱置,也就是當我們讀到比較 low priority 的資源時,就會先擱置他 (有 preload 的除外,稍後會說明),其他再按照讀取的順序去 load 他,直到 body 被加入到 document 為止,其他就按照讀取的順序依序進行。 - 範例截圖: 此處為隨機選取的一個網站測試,可以看到 Priority 並不代表實際 download 資源的順序 ![](https://i.imgur.com/2XgHDHL.png) - 非 tight mode: 按照 document body 中被呼叫的順序 Reference : https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/edit# ## 改變資源優先程度的方式 - preload、prefetch、preconnect 等等: (備註: 下面的小節會做個別用途的說明) 不同使用情境可以搭配他們和 as = "想要含有什麼 default priority 的 type" 來將 priority 改變成原始的設值,而非依照讀取到的順序來載入資源 - 範例: `<link rel="preload" href="/images/demo-image.png" as="image">` 而此處的優先順序設置會對應到上圖 (圖一) 所標註的 Priority,例如: as="font" 的 priority 會是 High,as="image" 的 priority 會是 low - fetchpriority : 可以用來改變相對的 priority,可以搭配上述的使用方式來使用 - 需要注意的是: 此處的 fetchpriority 並不是一個瀏覽器的絕對指令,而是一個告訴瀏覽器說"developer 偏好這樣的優先順序",實際上的順序還是會以瀏覽器的決定為主 - 範例: `<link rel="preload" href="./assets/login-logo.svg" as="image" fetchpriority='high'/>` 對照到圖一,可以發現如果將 fetchpriority 設置為 high,其 priority 會變成 high ![](https://i.imgur.com/tmIQlDd.png) - 潛在問題: 測試環境: Chrome 109.0.5414.120 (Official Build) (64-bit) 在 preload 資源上設定 priority 雖然可以看到最後 preload 的資源個別被 mark as 該對應的 priority (對應到圖一),但是仍然沒有改變他們相對載入的順序 - 推測原因: 可能是因為在 Chrome 中 preload 無條件被提前載入,而 fetchpriority 只是一個提供給瀏覽器說 developer 偏好這樣的 priority 的設定,瀏覽器不一定要遵守,所以看起來才沒有被調整 > The fetchpriority attribute is a hint and not a directive. The browser will try to respect the developer's preference. It is also possible that the browser will apply its preferences for resource priority as deemed necessary in case of conflicts. Reference: https://web.dev/priority-hints/ - 在沒有設定 preload 的情況下,設定 fetchpriority 時運作良好: ```html= <img src="./assets/tradebeyond-logo.svg" alt="TradeBeyond Logo" fetchpriority='low'/> <img class="w-auto h-[23px] mb-[32px]" src="./assets/login-logo.svg" alt="Brand image" fetchpriority='high'/> ``` 結果: ![](https://i.imgur.com/P5w9oda.png) ## Cache ### 使用情境: 希望可以在不增加 server 負擔的情況下,拿到我們先前 request 過的資料。 ![](https://i.imgur.com/kOlVwPm.png) ### Chrome 瀏覽器的四大 Cache: - Mermory Cache (它把資源存在 memory (RAM) 裡面,所以在效能上會比 disk cache 還要快) `並非所有瀏覽器都會實做的一種快取,但 Chrome 有實作所謂的 "Memory Cache"` - 那麼什麼東西"有機會"被放到 Memory Cache 裡面呢 ? 瀏覽器經由 preload 機制 or 先前 http request 過取得的資源都會被放在記憶體快取裡面優先詢問 - 請求獲取資源的限制: 別人在要求 Cache 中的資源時,只會核對想要取得的資源的 URL、檔案類型、CORS 模式和一些特性,就會將資源給予出去 - 需要注意的問題 - 時效性: Document 被關閉時 ( 關閉瀏覽器 or 頁籤時) 資料會被清空 - Service Worker Cache - 何謂 Service Worker ? 一層在瀏覽器跟 network 層級間的 proxy ,他有可以攔截使用者 request 的能力 (透過 listen to fetch event, ex: self.addEventListener('fetch', function(event) {//...略} )),developer 可以透過 js 實作 postMessage 來與 Service Worker 溝通 - 提供快取功能: Service Worker 提供 "快取" 的功能,可以透過攔截使用者發出的請求後,決定是否要回傳快取內容 (有關 Service Worker 的生命週期以及如何撰寫控制可以參考 https://ithelp.ithome.com.tw/articles/10276666) - HTTP Cache (Disk Cache) > HTTP Cache 需要伺服器端與瀏覽器端事先經過協商,至於協商的方式顧名思義就是透過 HTTP Request 來達成,瀏覽器與伺服器**透過在 HTTP Request 與 Response 的 header 帶入一些資訊來協商快取的機制** 因此在請求 http cache 中的資源時 (也就是協商的過程中),Server 會考慮到以下的 http header 相關設定再給予回應: ![](https://i.imgur.com/TIF2UZ9.png) - Cache-Control: 最常見的 Cache-Control 狀態: - public: 公開的資源,可以被所有節點暫存 -> 其他使用者用不同瀏覽器瀏覽該網站,資源會被集體 cache 到一起,之後又有其他人加入,就可以直接存取別人存取過的內容 - private: 私有的資源,只被允許儲存成使用者的本地快取 -> 相較於 public ,這只能提供給一個使用者載瀏覽器裡存取 - no-cache: 不得在與 Origin Server 成功驗證前,使用快取滿足「此請求」 誤區: no-cache 並不是指 "不能使用快取",而是指我們需要在請求的時候 "**先通過驗證**" 因此,只要訊息中使用條件請求 (Conditional Requests) 且通過驗證, 我們就能 "**直接覆用快取**",不需重新下載回應 - 補充說明: Conditional Request 可以參考: https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests 通常 header 帶有 Modified 相關元素 (if-Modified-Since) 會是 conditional request,因為帶有這些 header 的 request 會跟 Server 來互相確認是否有被 Modify 過,也就是該 request 會與 Server 有互動,會需要 Validator 來確認是否有被修改,Server 也會依據驗證後的條件來給予不同的回應 - no-store: 不開快取 因此,如果當 cache-control 被設定成 no-store 基本上就是禁止 http 快取了 範例: ![](https://i.imgur.com/5diTJmS.png) portal sandbox 範例: http cache 為 no-store 的情況下,沒有任何東西從Http Cache拿 ![](https://i.imgur.com/DjgIkAE.png) - max-age: 設定過期時間 範例: `Cache-Control: max-age=30` 設定過期時間為 30 秒 cas 範例: - 在有使用 preload 的情況下,request tradebeyond logo 收到了以下回應: 拿到的 http response 是 "max-age=0,must-revalidate",也就是一 request 馬上就過期,需要重新驗證,也就是需要重新再 request 一次並且通過驗證,且在這個情況下,我們也有收到了 no-store 不允取存取任何 http cache 資料的回應,所以基本上我們的資料都會馬上過期,這樣會被視為需要重新 request 資料,這也就是為什麼刷新後發現他都還是 status 200,重新 request 而沒有被存到 disk cache 的原因 ![](https://i.imgur.com/hwDEtKt.png) - 其他 Http Cache Header: - Expire: HTTP 1.0 就存在的 Header,用來處理判斷資源是否過期,需要重新 request 而非去 http cache 裡面請求資源 注意: 若 Expire 與 cache-control 的 max-age 同時存在,max-age 會覆蓋過 Expire 的效用 (相關資訊請查閱: https://www.rfc-editor.org/rfc/rfc9110.html) - Last-Modified 與 If-Modified-Since : 前後端互相配合的 Header,用來確認上一次修改是什麼時候 - Last-Modified: Server 回傳 response,並且帶有 Last-Modified 的 Header,表示這個檔案上一次更改是什麼時候。 - If-Modified-Since: 瀏覽器 request Server 時,就可以利用這個資訊來跟 Server 確認該資料是否在某個時間點後有被改動,如果沒有的話 Client 端會收到 304 Not Modified,如果有被改動且 Server 回傳資源成功的話,會收到 status 200 以及該資源 - 整合情境: 假設今天瀏覽器中的某個資源過期了,用了帶有 If-Modified-Since 的 header 重新 request server ,詢問是否有 "在某個時間點後更新的資料",如果有更新,Server 會回傳類似的 response (如以下範例): ``` GET /logo.png If-Modified-Since: 2017-01-01 13:00:00 ``` 如果沒有更新,就會回傳 `Status code: 304 (Not Modified)` ![](https://i.imgur.com/yAYxR5L.png) - Etag 一種 response header,是根據 token 所提供內容的版本——引號中的字符串,例如“657fa34d356c-aq32” - 如果資源經過修改,字符串會發生變化,瀏覽器會在快取過期後,發送 If-None-Match 詢問 Server 是否有新的資料(不符合這個Etag的資料),有的話就回傳新的,沒有的話就回傳 304 - 如果在發出請求之前 token 未更改,瀏覽器將繼續使用其本地版本 範例: ![](https://i.imgur.com/LQQbJLp.png) - Push “Cache” Push 快取(Push “Cache”)是 HTTP/2 儲存推送資源的地方,資源的保存不具永久性,只要 session 終止,資源就會被清除 - Server push 設定方式: 假設在可以設置 http push 的情況下,我們可以透過設置 Nginx 的 server push 來讓想要加載的資源加載速度加快,在端口多設置想要 server push 的資源: ``` server { listen 443 ssl http2; server_name localhost; ssl on; ssl_certificate /etc/nginx/certs/example.crt; ssl_certificate_key /etc/nginx/certs/example.key; ssl_session_timeout 5m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location / { root /usr/share/nginx/html; index index.html index.htm; // http2_push 多設置的部分: 想要 server push 的資源 http2_push /style.css; http2_push /example.png; } } ``` 或是可以在後端應用產生一個 link,在透過設定 http2_push_preload on 來開啟 http2 push (此處的運作方式會是: 如果服務器或者瀏覽器不支持 HTTP/2,那麼瀏覽器就會按照 preload 來處理這個 header message) ``` server { listen 443 ssl http2; root /var/www/html; location = / { proxy_pass http://upstream; http2_push_preload on; } } ``` (相關使用方式可以參考: https://www.readfog.com/a/1670238987148693504) - 開啟 dev tool 可以看到如下結果: ![](https://i.imgur.com/MaqB0dY.png) - Http/1 inline everything & Http/2 server push 的測試速度差異: ![](https://i.imgur.com/GW1PcjC.png) (圖片資源參考來源: https://www.smashingmagazine.com/2017/04/guide-http2-server-push/) 其他 http 2 push 可參考: - 有關 http/2 push cache 在不同瀏覽器的支援測試: https://blog.csdn.net/qq_17497931/article/details/107591197 - 有關 http/2 server push 需要注意的問題: https://www.evanlin.com/til-http2-server-push/ --- ## Preload、Prefetch、Preconnect ### Preload - 使用情境: 當我們想要在瀏覽器當前頁面 (current page) 進行即時轉譯,就立即可以使用某些內容,此時就可以考慮使用預先載入 (preload) 的方式 - 使用方式: 在 <link> 中使用 rel="preload",可以在此頁 current page 預先載入 (preload) 稍後需要使用的資源。在需要使用的時候再去使用他,例如在 body 裡面下一個 img tag 之類的 - 使用公式: `<link rel="preload" href="檔案來源" as="檔案類型" />` 以下為 preload js、video、mp4、font、image 的例子 : ```html= <head> <meta charset="utf-8" /> <title>JS and CSS preload example</title> <!--以下為 preload 預先載入 --> <link rel="preload" href="style.css" as="style" /> <!-- stylesheet --> <link rel="preload" href="main.js" as="script" /> <!-- script --> <link rel="preload" href="main.js" as="image" /> <!-- image --> <!-- MIME Type --> <link rel="preload" href="intel-short.mp4" as="video" type="video/mp4" /> <!-- mp4 --> <link rel="preload" href="fonts/foo.woff" as="font" type="font/woff" crossorigin="anonymous" /> <!-- woff font --> <link rel="preload" href="fonts/roboto-webfont.ttf" as="font" type="font/ttf" crossorigin="anonymous" /> <!-- ttf font --> <link rel="preload" href="fonts/roboto-webfont.svg" as="font" type="image/svg+xml" crossorigin="anonymous" /> <!-- svg font --> <!-- media query preload --> <link rel="preload" href="bg-image-narrow.png" as="image" media="(max-width: 600px)" /> <link rel="preload" href="bg-image-wide.png" as="image" media="(min-width: 601px)" /> <!-- preload 只是做預先下載的動作,還是自己使用它才有效 --> <link rel="stylesheet" href="style.css" /> </head> ``` 備註: 可以看到上述有一些使用到 crossorigin="anonymous" (例如: font),是因為 Same-Origin 同源政策的緣故,為了避免被擋下來通常會設成 anonymous,這表示沒有需要傳送任何驗證 credential 資料,而如果設成需要傳送 credential 資料 (cookie, a certificate, a HTTP Basic authentication) 的話,通常會用 crossorigin="use-credentials" - 適合使用 preload 的場景: 載入 css font file、圖片或是較大的影音檔案。 - 實例: 在需要預先載入 logo svg image 的情境下,我們以以下程式碼作為範例: ```html= <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- preload 預先載入我們想要使用的 logo img --> <link rel="preload" href="./assets/tradebeyond-logo.svg" as="image" /> <title>TradeBeyond</title> </head> <body class="box-border bg-gradient-to-b w-screen h-screen from-dark-blue to-light-blue"> <div class="w-screen h-screen flex flex-col items-center font-roboto"> <div class="flex-none flex w-full max-w-[1546px] h-[58px] py-[3px] px-[36px] justify-between"> <a class="flex w-[180px] min-w-[180px] relative top-0 left-0 items-center" href="https://tradebeyond.com/"> <!-- 使用我們想要使用的 logo img --> <img src="./assets/tradebeyond-logo.svg" alt="TradeBeyond Logo" /> </a> </div> </div> </body> ``` 在有 preload 的情況下,我們可以看到 Network 中的 waterfall 顯示我們是先載入 image 再載入 css 的 (先載入要 preload 的資源,再接下去下載 css): ![](https://i.imgur.com/I2f0iRk.png) 然而在沒有 preload 的情況下是這樣的 - 先載入了 css 再載入 image svg (會按照各資源在 HTML 中的順序載入): ![](https://i.imgur.com/t9nPfnp.png) ### Preload Priority 與在 waterfall 上順序不一致的問題: - 問題情境: 在 preload 兩個 Image: tradebeyond logo & login-logo ,同時並沒有針對 css 做 preload 處理的情況下,Image 的 Prioity 明明設為 High,相較 css 的 Priority 為 Highest,明明優先度比較低,為什麼還是先載入 Image ?! ![](https://i.imgur.com/jywkqq4.png) - 使用情境: 測試環境: Chrome 109.0.5414.120 (Official Build) (64-bit) 使用 preload = 是在 Chrome 95 版本後使用 preload - 原因: > preloads will load in the order the parser gets to them for anything above "Medium" priority Reference : https://web.dev/priority-hints/ 被 preload scanner 掃到的 resource ,全數會在 priority 為 Medium 以前的 resource 前被優先下載,無論這個 resource 是否有比較低的 priority。 - 在 Chrome 95 後有一些跟 preload 相關的 fix,此處有使用 preload 的注意事項: Reference : https://web.dev/priority-hints/#using-preload-after-chrome-95 ### Preload 與 Cache > preload 取得的資源,和一般的 HTTP request 使用相同的快取。所以資源如果可以快取的話會放在 HTTP cache 和 memory cache,不能快取的話,會在 memory cache。 (資料來源: https://shubo.io/preload-prefetch-preconnect/#caching-%E8%A1%8C%E7%82%BA) ### PreFetch - 使用情境: 當我們想要在瀏覽器下一頁 (next page) 使用到資源時預先載入,就可以使用 prefetch。 **資源將會等頁面完全下載完以後,以 Lowest 優先度下載。** 白話文例子: 使用者有很高的機率會點擊"下一頁",而下一頁的某某資源我等等會用到,瀏覽器請你有空的話幫我先下載。 - 使用方式: `<link rel="prefetch" href="檔案來源" as="檔案類型">` ### Preconnect - 使用情境: 此網頁在不久後可能會下載某個 domain 的資源,請先建立好連線 - 使用方式: `<link rel="preconnect" href="https://example.com">` - 為什麼需要 Preconnect ? - 資源下載流程需要經過: - 向 DNS 請求解析域名 - TCP handshake (可參考: https://www.yasssssblog.com/2020/10/04/ithome-30-21-tcp-udp/) - Https connection - 連線完成 -> 等待拿到資料的第一個byte - 在拿到 first byte 以前,我們都需要去等待一段時間 (3 個 RTT Round Trip Time 的來回時間),所以預先連線拿取資料可以避免我們在要獲取資源時,才去 connect 並等待資料出現 ## 最後的提醒 Summary - 使用須知 : - 避免踩雷: 先檢查一下 Server 的 config 相關設定,如果有設定 no-store ,可能需要考慮一下使用情況與最終想要達成的目標,因為 preload 有可能不適合使用 XD