今天要介紹的是 PRPL 模式,這也是和優化效能有關的模式。
PRPL 模式是由 Google Chrome 團隊所發展提出的,目的是提供更快的網絡體驗。這個模式是隨著 Service Workers、後台同步、Cache API、Priority hints 和 pre-fetching 等技術出現而發展出來的,此模式可確保在低端設備或網路不佳的情況下,應用程式也能正常載入。PRPL 模式包括 4 個主要操作:Push(or preload)、Render、Pre-cache 和 Lazy-load,因為 PRPL 就是關注這 4 個要素:
我們可為關鍵資源加上 preload
的 resource hint,告訴瀏覽器要盡早獲取這些資源,在 HTML 文件的 head
中加入包含 rel="preload"
的 <link>
標記,即可告訴瀏覽器預先載入此關鍵資源。
<link rel="preload" href="example-image.jpg" as="image">
<link rel="preload" href="emoji-picker.js" as="script">
而如果要在 React 中使用 preload
,React 官方有提出 Canary 和實驗性版本的 preload API,不過因為是實驗性質,正式環境可能還不適合使用。React 官方有提及,如果你是用 React-based 的框架來開發(例如:Next.js、Remix),通常框架都會幫你處理好資源的載入,因此開發者很少需要自己呼叫 preload
的 API。如果要在 React 使用 preload
的方法,可以這樣用:
// 從 react-dom 匯入 preload
import { preload } from 'react-dom';
function AppRoot() {
// preload 第一個參數是 href,第二個參數是 options,可填入的 options 請見官方文件 https://react.dev/reference/react-dom/preload#parameters
preload("https://example.com/font.woff2", {as: "font"});
// ...
}
href
與 as
attribute<link>
的 href
屬性會填入此資源的路徑,as
的屬性則代表此資源的類型,as
属性可能的值包含:audio
、font
、image
、script
、style
、worker
等,而 as
屬性填入的資源類型也會影響瀏覽器對於資源的載入順序。以下是 Chrome 對各資源的載入優先級排序。
表 1 Chrome 針對各資源的載入優先級排序(資料來源:https://web.dev/articles/fetch-priority)
資源類型 | 載入優先級 |
---|---|
主資源 | VeryHigh |
CSS(early)** | VeryHigh |
Font | VeryHigh |
Script(early 或不是來自 preload scanner) *** | High |
Image(位於可視區域,在佈局完成後發生) | High |
XHR/fetch(async) | High |
CSS(late)** | Medium |
Image(前5個超過10,000px²的圖片) | Medium |
Script(async/defer) | Low |
Image | Low |
Media (video/audio) | Low |
CSS(media mismatch)**** | Low |
Prefetch | VeryLow |
** early & late:early 指的是在任何非預加載的圖片被請求之前就發出的請求。網頁載入時,瀏覽器會先處理一些資源(如CSS、JS等)。如果這些資源是在非預加載的圖片被請求之前就已經請求了,那這些資源就被視為早期資源,例如在
<head>
中的 CSS。相對而言,late 指的是在非預加載的圖片請求之後。
*** 不是來自 preload scanner:preload scanner 是瀏覽器中的一個機制,它能夠在 HTML 解析的過程中,並行預加載某些資源。不是來自 preload scanner 代表這些 script 不會通過 preload scanner 提前載入,而是需要等到 HTML 解析到它們的時候才會被請求。
**** media mismatch:指的是那些媒體類型不匹配的 CSS。媒體類型(media type)通常由 CSS 的 media 屬性定義,例如 media="screen" 或 media="print"。當 media 尺寸不符合時,瀏覽器會忽略這些文件的預加載,因為這些樣式暫時不適用於當前的設備或環境。
另外,關於圖片載入的優先級,在預設情況下,圖片載入的優先級是 Low,但如果它們在可視範圍內,佈局完成後會提升為 High。Chrome 117 開始,前 5 張大圖片的優先級會是 Medium,這樣可以加快它們的載入速度。如果你希望一些關鍵圖片一開始就以 High 優先級加載,可以使用 fetchpriority="high"
屬性來加速它們的載入。
preload 與 prefetch 都是用來預先載入資源的 resource hint,兩者差異如下:
表 2 preload 與 prefetch 差異(資料來源:自行繪製)
preload | prefetch | |
---|---|---|
急迫性 | 強制性、急迫性更高,無論如何都會被預載入 | 屬於 Resource Hint,建議瀏覽器去做,較不急迫;瀏覽器會根據網路連線和 bandwidth 是否足夠來決定是否要預取資源 |
資源類型 | 通常是需要立即使用的資源,如初始渲染中使用的字體,或使用者立即能看到的某些圖片 | 抓取的資源不限於當前頁面使用,可以跨越頁面 |
載入時機 | 現在立刻馬上就要下載 | 什麼時候要下載,交由瀏覽器自行決定 |
可用 preload
搭配 async
來讓瀏覽器高優先下載 script,又不阻擋剖析器等待 script。
<link rel="preload" href="example.js" as="script">
<script src="example.js" async></script>
<link rel="preload">
:告訴瀏覽器盡早開始下載指定的資源,讓它能提前載入
<script async>
:用來執行 script
<script src="example.js" async>
時,如果這個資源已經被 <link rel="preload">
預載入過,瀏覽器會直接使用已經下載的資源,而不會再次下載兩者結合使用時,瀏覽器會先預載入資源,然後再執行,並且不會重複下載同一個資源。
《JavaScript 設計模式學習手冊 第二版》一書中有提及對 preload
的建議與注意事項如下:
preload
會按照撰寫的順序載入
preload
都應小心放置於 HTML <head>
,以免影響其他更緊急資源的載入preload
最好放在 <head>
的結尾或 <body>
的開頭
preload
會有較低的優先級,應該合理安排圖片 preload
的順序,確保它不會拖慢網頁中更關鍵資源的載入速度,例如影響網頁互動的 JavaScriptpreload
改善互動性時,可能會延遲其他資源(如:圖片、字體)的載入,而影響了首次內容繪製(First Contentful Paint, FCP)或最大內容繪製(Largest Contentful Paint, LCP)的指標,因此須謹慎使用 preload在訪問網站時,一般流程如下:
圖 1 訪問網站的一般流程(資料來源:自行繪製)
這個訪問網站流程有哪裡可優化呢? 可以看出瀏覽器需要多次向伺服器請求資源(style、script),多次向伺服器請求資源會增加網路往返的次數(Round Trip Time, RTT),導致整個網頁的載入時間變長,因此我們應避免多次向伺服器請求資源,減少瀏覽器和伺服器間的往返次數。而其中一個優化方式就是利用 HTTP/2 協議的 Server Push 技術。
原先是瀏覽器爬取 HTML 時發現還要 style 和 script 檔案時,再向伺服器發出請求,但 server push 可做到讓伺服器自動發送額外資源給客戶端,客戶端就不需要每次明確請求資源,節省了網路往返的次數。流程如下:
index.html
index.html
,並另外 push style.css
、index.js
index.html
檔案發現需要 style.css
、index.js
,就可從快取中取得資源,不須再向伺服器請求
圖 2 Server Push 的流程(資料來源:自行繪製)
不過 Server Push 也有其缺點,server push 無法感知 HTTP cache,有時即使 HTTP header 中關於快取的資訊顯示此資源還沒過期,伺服器仍然會照樣推送,導致資源重複推送,client 端重複下載一樣的、沒過期的資源,造成網路頻寬的浪費。另外,server push 能推送的資源類型是有限制的,資源需要能被快取,且不能帶有 response body,因此只能推送 GET 與 HEAD 的請求。
PRPL 模式對此的解法是,在初始載入後使用 Service Worker 來快取資源,在瀏覽器發出請求時攔截不必要請求,在伺服器推送重複資源時避免下載、剖析不必要的資源。
補充:Service Worker
Service Worker 屬於 Web Workers 的一種,它是一層瀏覽器與網路請求間的 proxy,可攔截瀏覽器請求並快取資源,更多介紹此篇文章後半會再提及。
如何盡快渲染(Render)初始路由?可參考以下方式。
可以將 bundle 拆分為更小的檔案,只載入需要資源的 bundle,這部分在前面的程式碼拆分文章有提過如何拆分 bundle,並依照需求載入。
而除了拆分程式碼,另外一個降低初始 bundle 大小的方式,就是讓某些 JavaScript 的程式碼在伺服器端執行,而不是傳到瀏覽器再執行,以此減少傳給瀏覽器的 bundle 大小,這和渲染模式比較有關,會在之後文章繼續說明。
快取時要注意的是,如果快取了一個較大的 bundle 可能會遇到一個問題,就是多個 bundle 間可能會共享相同的資源,瀏覽器會無法辨別 bundle 內哪些部份是被共享的,這些共享資源可能會因為每個 bundle 的不同而重複下載,無法被有效快取,因此增加了與伺服器的往返次數,影響效能。
PRPL 模式因此強調每次請求的 bundle 應包含當前所需的最少資源,確保這些資源可以被快取。
如果 bundle 過大或過於複雜,可能會降低效能。
其他關於快取的敘述請見下方。
Proxy 模式的文章中有提到快取這個概念,快取的概念就是先在某個空間儲存需要被經常存取的資料,當之後需要請求這資料時,會先到這個快取的儲存空間尋找,如果有就直接拿該資料,如果沒有才發出請求去取得資料。
圖 3 快取示意圖(資料來源:自行繪製)
如果大略分類網頁應用的快取類型,可以分為以下(以下按照的順序是當使用者從瀏覽器發出請求時,會經過的順序):
快取的詳細介紹可參考文末 Reference 的資源,這裡先不展開太多。
回到 Pre-Cache,其意思是我們可以預先快取一些使用者未來會經常需要存取的資源,之後使用者需要時就能盡快拿到這資料,又或是如果使用者突然斷網,也可以透過快取取得資料而不會畫面突然空白。而要如何做到呢?我們可以用 Service Worker 來做這類的快取控制,會是 Service Worker 而不是 HTTP cache 或 CDN cache,是因為開發者可以對它有很高的控制權,能撰寫程式碼來控制快取的條件、行為等等,也能因此達到離線瀏覽的功能。
Service Worker 屬於 Web Workers 的一種。Web Workers 是什麼?在瀏覽器環境中,除了執行 JavaScript 的 JacaScript 引擎,還有其他運作的引擎如:渲染 engine、處理 HTTP request 的引擎等,這些引擎可以不受 JavaScript 程式的影響而在背後執行自己的任務,而 Web Workers 可以想成是另一個執行任務的引擎,他會在瀏覽器開出一條新的 thread 來執行 JavaScript,兩條 thread 不會互相影響,因此不會干擾使用者介面的運行。
Service Worker 又是什麼呢?如上所述,它屬於 Web Workers 的一種,可以在瀏覽器背後執行 JavaScript,獨立於主瀏覽器線程(main thread)之外。其特色在於它會在 Web 應用程式與網絡之間扮演一層 proxy,透過監聽 fetch 事件,它能攔截瀏覽器發出的網絡請求,且它能利用 Cache API 來達到快取功能,可以在攔截瀏覽器發出的請求後決定要不要回傳快取的內容。
擁有正確緩存/快取策略的 Service Worker 可以為各種情境提供更好的使用者體驗,例如:立即回傳 pre-cache 的資源、在快取中儲存資料,以及在連接到網路時更新資料。
圖 4 Service Worker 示意圖(資料來源:自行繪製)
怕篇幅展開太多,這篇先不介紹 Service Worker 的實作範例,對如何自己撰寫 Service Worker 程式碼有興趣的可參考 Day18 X Service Workers Cache 和 Service Worker:從入門到放棄,但如果不想要自己寫複雜的程式碼,也可以使用 Google 提供的服務 Workbox,它提供了一系列工具,可讓開發者更快速的建立和維護 Service Worker 來快取資料。
在需要的時候才惰性載入路由、bundle 或是靜態資料,這部分在動態匯入的文章有介紹過,就不再多說。但這裡提一下在 <img>
標籤中我們可以加入 loading
的屬性來設定是要惰性插入還是要急迫載入此圖片。
<img src="example.png" loading="lazy" alt="…" width="150" height="150">
loading
屬性可填入兩種值:
lazy
:延遲載入,直到資源到達距離可視區域(viewport)一定距離時才載入eager
:預設行為,沒填入 loading
屬性時預設是 eager
,不論圖片位於頁面的哪個位置,都會立即載入。雖然是預設值,但在某些情況下可以明確設定,例如如果你的程式碼檢查工具對沒有明確設置 loading
屬性而報錯時,就可設定此值更多關於圖片 loading
屬性的說明可見此篇文章。
總結來說,PRPL 對網頁應用的建議是: