網頁開發人員必須做出的其中一項核心決策,就是在應用程式中實作邏輯和算繪的所在位置。這可能很困難,因為建立網站的方式有很多種。
我們在過去幾年與大型網站的對談中,瞭解到 Chrome 在這個領域的運作方式。總體而言,我們建議開發人員考慮使用伺服器端轉譯或靜態轉譯,而非採用完整的重新活化方法。
為了進一步瞭解我們在做出這項決定時所選擇的架構,我們需要充分掌握每種方法,並在討論時使用一致的術語。轉譯方法的差異有助於從網頁效能角度說明在網路上轉譯的取捨。
術語
首先,我們要定義一些用語。
轉譯
- 伺服器端算繪 (SSR)
- 在伺服器上算繪應用程式,以便將 HTML 而非 JavaScript 傳送至用戶端。
- 用戶端轉譯 (CSR)
- 在瀏覽器中算繪應用程式,使用 JavaScript 修改 DOM。
- 補充水分
- 在用戶端上「啟動」JavaScript 檢視畫面,以便重複使用由伺服器轉譯的 HTML DOM 樹狀結構和資料。
- 預先算繪
- 在建構期間執行用戶端應用程式,以便將其初始狀態擷取為靜態 HTML。
成效
- 首次傳送位元組時間 (TTFB)
- 點選連結到新頁面載入第一個內容位元組之間的時間。
- 首次顯示內容所需時間 (FCP)
- 要求的內容 (文章內文等) 顯示的時間。
- Interaction to Next Paint (INP)
- 這項指標可評估網頁是否能持續快速回應使用者輸入內容。
- 總封鎖時間 (TBT)
- 代理程式指標 (INP):計算主執行緒在網頁載入期間遭到封鎖的時間長度。
伺服器端轉譯
伺服器端轉譯會針對導覽作業,為伺服器上的網頁產生完整 HTML。這樣可避免在用戶端上進行額外資料擷取和設定範本的往返作業,因為轉譯器會在瀏覽器取得回應前處理這些作業。
伺服器端轉譯作業通常可產生快速的 FCP。在伺服器上執行頁面邏輯並進行轉譯,可避免將大量 JavaScript 傳送至用戶端。這有助於減少網頁的 TBT,也可能導致 INP 降低,因為主執行緒在網頁載入期間不會經常遭到阻斷。主要執行緒的阻斷次數越少,使用者互動執行的機會就越早。這很合理,因為使用伺服器端算繪時,您實際上只是將文字和連結傳送至使用者的瀏覽器。這種做法適用於各種裝置和網路狀況,並可開啟有趣的瀏覽器最佳化功能,例如串流文件剖析。
使用伺服器端算繪後,使用者較不容易在使用網站前,等待 CPU 相關 JavaScript 執行完畢。即使您無法避免使用第三方 JS,透過伺服器端轉譯來降低自己的第一方 JavaScript 成本,也能為其他部分提供更多預算。不過,這種做法可能會產生一個權衡:在伺服器上產生網頁需要時間,可能會增加網頁的 TTFB。
伺服器端算繪是否足以滿足應用程式需求,很大程度上取決於您要打造的體驗類型。關於伺服器端轉譯和用戶端轉譯的正確應用方式,一直存在著長期爭論,但您可以隨時選擇為部分網頁使用伺服器端轉譯,而非其他網頁。部分網站已採用混合式算繪技術,並獲得成功。舉例來說,Netflix 會透過伺服器轉譯相對靜態的到達網頁,同時prefetching互動頻繁的網頁 JS,讓這些由用戶端轉譯的較重網頁更有機會快速載入。
許多新式架構、程式庫和架構可讓您在用戶端和伺服器上算繪相同的應用程式。您可以使用這些技巧進行伺服器端算繪。不過,在伺服器和用戶端上同時進行轉譯的架構是屬於不同類別的解決方案,其效能特徵和取捨也大不相同。React 使用者可以使用伺服器 DOM API,或以此為基礎的解決方案 (例如 Next.js) 進行伺服器端轉譯。Vue 使用者可以使用 Vue 的伺服器端轉譯指南或 Nuxt。Angular 有通用。不過,大多數熱門解決方案都會使用某種形式的補充,因此請留意工具使用的做法。
靜態轉譯
靜態算繪會在建構期間執行。只要您限制網頁上的用戶端 JS 數量,這個方法就能提供快速的 FCP,以及較低的 TBT 和 INP。與伺服器端算繪不同,這種方法也能持續提供快速的 TTFB,因為網頁的 HTML 不必在伺服器上動態產生。一般來說,靜態算繪是指事先為每個網址產生單獨的 HTML 檔案。您可以預先產生 HTML 回應,然後將靜態轉譯結果部署至多個 CDN,充分利用邊緣快取功能。
靜態轉譯解決方案的形狀和大小不盡相同。Gatsby 等工具的設計目的,是讓開發人員感覺應用程式是以動態方式算繪,而非在建構步驟中產生。11ty、Jekyll 和 Metalsmith 等靜態網站產生工具採用靜態特性,提供更多範本導向的方法。
靜態算繪的缺點之一,就是必須為每個可能的網址產生個別的 HTML 檔案。但如果無法預先預測這些網址,或是網站上有大量不重複的網頁,這麼做可能會很困難,甚至無法實現。
React 使用者可能熟悉 Gatsby、Next.js 靜態匯出或 Navi,這些工具都能方便地從元件建立頁面。不過,靜態轉譯和預先轉譯的行為有所不同:靜態轉譯的網頁可進行互動,且不需要執行太多用戶端 JavaScript;而預先轉譯可改善單一頁面應用程式的 FCP,該應用程式必須在用戶端上啟動,才能讓網頁真正具備互動性。
如果不確定特定解決方案是靜態算繪或預先算繪,請嘗試停用 JavaScript,然後載入要測試的網頁。對於靜態轉譯的網頁,即使沒有 JavaScript,大多數互動功能仍會存在。預先算繪的網頁可能仍保留一些基本功能,例如已停用的 JavaScript 連結,但大部分的網頁都處於靜止狀態。
另一個實用的測試方法是使用 Chrome 開發人員工具中的網路節流功能,查看網頁變得可互動前下載了多少 JavaScript。預先算繪通常需要更多 JavaScript 才能變成互動式,而 JavaScript 往往比靜態算繪中使用的漸進式增強方法更複雜。
伺服器端轉譯與靜態轉譯
由於伺服器端算繪的動態性質可能會產生大量運算額外成本,因此並非所有情況都適合採用。許多伺服器端轉譯解決方案不會提早沖洗、延遲 TTFB,或將傳送的資料加倍 (例如,JavaScript 在用戶端上使用的內嵌狀態)。在 React 中,renderToString()
是同步和單執行緒的,因此速度可能較慢。較新的 React 伺服器 DOM API 支援串流功能,可在伺服器上產生其餘部分的同時,將 HTML 回應的初始部分提早傳送至瀏覽器。
要讓伺服器端算繪「正確」,您可能需要尋找或建構元件快取解決方案、管理記憶體用量、使用Memoization 技術,以及處理其他問題。您通常會處理或重建相同應用程式兩次,一次在用戶端,一次在伺服器上。雖然伺服器端轉譯可讓內容更快顯示,但不一定會減少工作量。如果伺服器產生的 HTML 回應抵達用戶端後,您仍需在用戶端上執行大量工作,這可能會導致網站的 TBT 和 INP 更高。
伺服器端轉譯會根據需求為每個網址產生 HTML,但速度可能比僅提供靜態轉譯內容還要慢。如果您願意投入額外心力,伺服器端算繪加上 HTML 快取可大幅縮短伺服器算繪時間。相較於靜態轉譯,伺服器端轉譯的好處在於,您可以擷取更多「即時」資料,並回應更完整的一系列要求。需要個人化功能的網頁就是無法與靜態算繪搭配使用的具體要求類型範例。
在建構 PWA 時,伺服器端轉譯功能也可以提供有趣的決策:使用全頁服務工作程快取功能,還是只在伺服器端轉譯個別內容?
用戶端轉譯
用戶端轉譯是指直接在瀏覽器中使用 JavaScript 轉譯網頁。所有邏輯、資料擷取、範本和路由都會在用戶端處理,而不是在伺服器上處理。實際結果是,從伺服器傳遞至使用者裝置的資料會增加,但這會帶來一些取捨。
用戶端算繪作業很難在行動裝置上順利進行,只要稍微調整JavaScript 預算,並盡可能以最少的往返次數傳遞價值,就能讓用戶端算繪幾乎複製純伺服器端算繪的成效。您可以使用 <link rel=preload>
提供重要指令碼和資料,讓剖析器更快為您服務。我們也建議您考慮使用 PRPL 等模式,確保初始和後續導覽作業能即時完成。
用戶端轉譯的主要缺點是,隨著應用程式規模擴大,所需的 JavaScript 數量也會增加,進而影響網頁的 INP。若要加入新的 JavaScript 程式庫、polyfill 和第三方程式碼,就會特別困難,因為這些元件會爭奪處理效能,且通常必須在頁面內容算繪前處理。
使用用戶端算繪且仰賴大型 JavaScript 套件的體驗,應考慮採用積極的程式碼分割,以便在網頁載入期間降低 TBT 和 INP,並在需要時採用延後載入 JavaScript,只提供使用者需要的內容。如果是互動性不高或完全沒有互動性的體驗,伺服器端轉譯可能會是這些問題更具可擴充性的解決方案。
如果您是建構單頁應用程式,只要找出大部分網頁共用的使用者介面核心部分,就能套用應用程式殼層快取技巧。結合服務工作者,這可以大幅改善重複造訪時的效能,因為網頁可以快速載入應用程式殼 HTML 和 CacheStorage
的依附元件。
重新整理功能結合了伺服器端和用戶端轉譯
Rehydration 是一種方法,可同時執行用戶端和伺服器端轉譯,以便平衡兩者之間的取捨。瀏覽要求 (例如全頁載入或重新載入) 會由將應用程式轉譯為 HTML 的伺服器處理,然後將用於轉譯的 JavaScript 和資料嵌入產生的文件中。只要謹慎執行,就能達到快速的 FCP,就像伺服器端轉譯一樣,然後在用戶端上再次轉譯,以便「接收」。這是有效的解決方案,但可能會造成相當大的效能缺點。
使用重新活化功能的伺服器端算繪的主要缺點,是即使可以改善 FCP,但仍可能對 TBT 和 INP 造成嚴重負面影響。雖然伺服器端轉譯的網頁似乎已載入並可互動,但在執行元件用戶端指令碼並附加事件處理常式之前,這些網頁實際上無法回應輸入內容。在行動裝置上,這可能需要花上數分鐘,讓使用者感到困惑和沮喪。
復水問題:以兩個應用程式的價格購買一個應用程式
為了讓用戶端 JavaScript 準確地「接手」伺服器中斷的地方,而不必重新要求伺服器轉譯 HTML 時所用的所有資料,大多數的伺服器端轉譯解決方案會將 UI 資料依附元件的回應序列化為文件中的指令碼標記。由於這會複製大量 HTML,因此重新啟用會造成更多問題,而非只是延遲互動性。
伺服器會針對導覽要求傳回應用程式 UI 的說明,但也會傳回用於組合該 UI 的原始資料,以及 UI 實作的完整副本,以便在用戶端啟動。bundle.js
完成載入及執行作業後,UI 才會變成可互動。
從實際網站收集的效能指標顯示,使用伺服器端算繪和復水功能,很少是最佳選項。最主要的原因是,當網頁看起來已準備就緒,但其中的互動功能都無法運作時,會對使用者體驗造成影響。
不過,透過重新活化功能,伺服器端轉譯也能達到相同效果。在短期內,只有針對高度可快取的內容使用伺服器端算繪,才能降低 TTFB,產生與預先算繪相似的結果。逐漸或部分重新活化,或許是讓這項技術在未來更實用的關鍵。
串流伺服器端轉譯作業,並逐步重新載入
過去幾年,伺服器端轉譯功能已取得許多進展。
串流伺服器端算繪功能可讓您以區塊傳送 HTML,瀏覽器可在收到 HTML 時逐漸算繪。這樣一來,使用者就能更快收到標記,加快 FCP 的速度。在 React 中,如果在 renderToPipeableStream()
中為非同步串流,相較於同步 renderToString()
,表示回壓處理得宜。
漸進式重整也值得考慮,React 已實作這項功能。採用這種做法後,伺服器算繪應用程式的個別部分會隨著時間「啟動」,而非採用目前常見的做法,一次初始化整個應用程式。這麼做可減少讓網頁具備互動功能所需的 JavaScript 用量,因為您可以延後用戶端對網頁中低優先順序部分進行升級,避免阻斷主執行緒,讓使用者在發起互動後,能更快進行互動。
漸進式重新調整功能還可協助您避免最常見的伺服器端轉譯重新調整陷阱:伺服器轉譯的 DOM 樹狀結構會遭到銷毀,然後立即重建,這通常是因為初始同步的用戶端轉譯所需資料尚未準備就緒,通常是尚未解析的 Promise
。
部分復水
部分復水功能實作起來相當困難。這種做法是漸進式重新活化功能的延伸,可分析網頁的個別部分 (元件、檢視畫面或樹狀結構),並找出互動性不高或沒有回應性的部分。對於這些大多為靜態的部分,對應的 JavaScript 程式碼會轉換為惰性參照和裝飾性功能,將其用戶端足跡降至幾乎為零。
部分補充方法有其自身的問題和妥協。這會對快取作業造成一些有趣的挑戰,而用戶端導覽功能則意味著我們無法假設應用程式中無活動部分的伺服器轉譯 HTML 會在整個網頁載入前就提供。
三元論算繪
如果您考慮使用服務工作者,請考慮採用三態算繪。這項技術可讓您針對初始或非 JS 導覽使用串流伺服器端轉譯,然後在服務工作者安裝後,讓服務工作者負責轉譯導覽的 HTML。這可讓快取的元件和範本保持最新狀態,並啟用 SPA 樣式的導覽,以便在同一個工作階段中轉譯新檢視畫面。在伺服器、用戶端網頁和服務工作者之間共用相同的樣板和路由程式碼時,這個方法最有效。
SEO 考量因素
選擇網頁算繪策略時,團隊通常會考量 SEO 的影響。伺服器端算繪是提供「完整外觀」體驗的熱門選擇,可供檢索器解讀。檢索器可以解讀 JavaScript,但在呈現方式上通常會有限制。用戶端算繪雖然可行,但通常需要額外的測試和額外負擔。如果您的架構高度仰賴用戶端 JavaScript,最近的動態轉譯也成為值得考慮的選項。
如有疑問,請使用行動裝置相容性測試工具,測試所選方法是否能達到預期效果。這項功能會以視覺化方式預覽任何網頁在 Google 檢索器中顯示的樣貌、執行 JavaScript 後找到的序列化 HTML 內容,以及轉譯期間遇到的任何錯誤。
結論
決定算繪方法時,請評估並瞭解瓶頸。請考量靜態轉譯或伺服器端轉譯是否能讓您達到這個目標。您可以將 HTML 和少量 JavaScript 整合在一起,以便提供互動式體驗。以下是顯示伺服器-用戶端範圍的實用資訊圖:
抵免額
感謝大家提供意見和靈感:
Jeffrey Posnick、Houssein Djirdeh、Shubhie Panicker、Chris Harrelson 和 Sebastian Markbåge