發布日期:2019 年 2 月 6 日,上次更新日期:2026 年 1 月 5 日
網頁程式開發人員必須做出的核心決策之一,是在應用程式中實作邏輯和轉譯的位置。由於建立網站的方式有很多種,因此這可能很困難。
過去幾年,我們在 Chrome 團隊與大型網站溝通,因此對這個領域有所瞭解。一般來說,我們建議開發人員考慮使用伺服器端轉譯或靜態轉譯,而非完整重新 Hydration 的做法。
為了更清楚瞭解我們在做出這項決定時所選擇的架構,我們需要一致的術語和每種方法的共用架構。接著,您就能從網頁效能的角度,更妥善地評估每種轉譯方法的優缺點。
術語
首先,我們會定義一些要使用的術語。
轉譯
- 伺服器端算繪 (SSR)
- 在伺服器上轉譯應用程式,將 HTML (而非 JavaScript) 傳送至用戶端。
- 用戶端轉譯 (CSR)
- 在瀏覽器中算繪應用程式,並使用 JavaScript 修改 DOM。
- 預先算繪
- 在建構時執行用戶端應用程式,以靜態 HTML 擷取初始狀態。請注意,這裡的「預先算繪」與瀏覽器預先算繪未來導覽不同。
- 飲水量
- 執行用戶端指令碼,將應用程式狀態和互動功能新增至伺服器轉譯的 HTML。Hydration 假設 DOM 不會變更。
- 重新佈建
- 雖然 rehydration 通常與 hydration 意思相同,但 rehydration 意味著定期以最新狀態更新 DOM,包括在初始 hydration 之後。
效能
- Time to First Byte (TTFB)
- 從點選連結到新網頁載入第一個內容位元組的時間。
- First Contentful Paint (FCP)
- 要求內容 (文章內文等) 顯示的時間。
- Interaction to Next Paint (INP)
- 這項指標可評估網頁是否能持續快速回應使用者輸入內容。
- 總封鎖時間 (TBT)
- INP 的代理指標,用於計算載入網頁期間主執行緒遭到封鎖的時間長度。
伺服器端轉譯
伺服器端轉譯會根據導覽,在伺服器上產生網頁的完整 HTML。這樣一來,轉譯器會在瀏覽器收到回應前處理這些作業,避免在用戶端上擷取資料和範本時,需要額外的來回作業。
伺服器端轉譯通常會產生快速的 FCP。在伺服器上執行網頁邏輯和轉譯,可避免將大量 JavaScript 傳送至用戶端。這有助於減少網頁的 TBT,也可能降低 INP,因為在載入網頁期間,主執行緒不會經常遭到封鎖。主要執行緒遭到封鎖的頻率降低時,使用者互動就有更多機會能更快執行。
這很合理,因為使用伺服器端算繪時,您實際上只是將文字和連結傳送至使用者的瀏覽器。這種做法適用於各種裝置和網路條件,並可開啟有趣的瀏覽器最佳化功能,例如串流文件剖析。
採用伺服器端轉譯,使用者就不太需要等待 CPU 繫結 JavaScript 執行,即可使用網站。即使無法避免第三方 JavaScript,使用伺服器端轉譯來減少第一方 JavaScript 成本,也能為其他部分提供更多預算。不過,這種做法可能會有一個缺點:在伺服器上產生網頁需要時間,這可能會增加網頁的 TTFB。
伺服器端轉譯是否足以滿足應用程式需求,主要取決於您要建構的體驗類型。關於伺服器端轉譯和用戶端轉譯的正確應用方式,一直以來都有爭議,但您隨時可以選擇對某些網頁使用伺服器端轉譯,其他網頁則不使用。部分網站已成功採用混合式轉譯技術。舉例來說,Netflix 會伺服器端轉譯相對靜態的到達網頁,同時prefetching互動頻繁頁面的 JavaScript,讓這些較重的用戶端轉譯頁面更有機會快速載入。
許多現代架構、程式庫和架構都可讓您在用戶端和伺服器上算繪相同的應用程式。您可以將這些技術用於伺服器端轉譯。不過,在伺服器「和」用戶端上進行轉譯的架構,本身就是一類解決方案,具有截然不同的效能特性和權衡。React 使用者可以使用伺服器 DOM API,或以其為基礎建構的解決方案 (例如 Next.js),進行伺服器端轉譯。Vue 使用者可以參考 Vue 的伺服器端轉譯指南或 Nuxt。Angular 則有 Universal。
不過,大多數熱門解決方案都會使用某種形式的水合,因此請留意工具使用的方法。
靜態算繪
靜態轉譯會在建構時進行。只要限制網頁上的用戶端 JavaScript 數量,這種做法就能提供快速的 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 回應的初始部分傳送至瀏覽器,而其餘部分仍在伺服器上產生。
如要「正確」執行伺服器端轉譯,可能需要尋找或建構元件快取解決方案、管理記憶體用量、使用記憶化技術,以及考量其他問題。您通常會處理或重建同一個應用程式兩次,一次在用戶端,一次在伺服器。伺服器端轉譯能更快顯示內容,但不一定能減少工作量。如果伺服器產生的 HTML 回應抵達用戶端後,用戶端仍有大量工作,網站的 TBT 和 INP 仍可能偏高。
伺服器端轉譯會根據需求為每個網址產生 HTML,但速度可能比直接提供靜態轉譯內容慢。如果您願意多花點功夫,伺服器端轉譯加上 HTML 快取,可大幅縮短伺服器轉譯時間。伺服器端轉譯的優點是,相較於靜態轉譯,伺服器端轉譯能提取更多「即時」資料,並回應更完整的請求。需要個人化的網頁,就是不適合靜態轉譯的要求類型。
建構 PWA 時,伺服器端轉譯也可能帶來有趣的決策。使用全頁 Service Worker 快取,還是伺服器轉譯個別內容片段比較好?
用戶端算繪
用戶端轉譯是指直接在瀏覽器中使用 JavaScript 轉譯網頁。所有邏輯、資料擷取、範本和轉送作業都是在用戶端處理,而非伺服器。實際結果是,伺服器會將更多資料傳遞至使用者的裝置,而這會帶來一組權衡。
在行動裝置上,用戶端算繪可能難以快速完成和維持。
只要稍微努力,嚴格控制 JavaScript 預算,並盡可能減少來回次數,就能讓用戶端算繪幾乎達到純伺服器端算繪的效能。您可以使用 <link rel=preload> 傳送重要指令碼和資料,讓剖析器更快為您工作。我們也建議您考慮使用 PRPL 等模式,確保初始和後續導覽都能即時完成。
用戶端轉譯的主要缺點是,所需的 JavaScript 量往往會隨著應用程式成長而增加,這可能會影響網頁的 INP。如果加入新的 JavaScript 程式庫、polyfill 和第三方程式碼,這項工作會變得特別困難,因為這些程式碼會爭奪處理能力,而且通常必須先處理,網頁內容才能算繪。
如果體驗使用用戶端轉譯,且依賴大型 JavaScript 組合,建議考慮積極分割程式碼,降低網頁載入期間的 TBT 和 INP,並延遲載入 JavaScript,只在使用者需要時提供所需內容。對於互動性極低或沒有互動性的體驗,伺服器端轉譯可提供更具擴充性的解決方案。
如果建構單頁應用程式,找出大多數頁面共用的使用者介面核心部分,即可套用應用程式殼層快取技術。搭配 Service Worker 使用,可大幅提升使用者再次造訪時的感知效能,因為網頁可以快速從 CacheStorage 載入應用程式殼層 HTML 和依附元件。
Rehydration 結合了伺服器端和用戶端算繪
「凍結」是一種同時進行用戶端和伺服器端轉譯的方法,可減少兩者之間的取捨。導覽要求 (例如完整網頁載入或重新載入) 會由伺服器處理,伺服器會將應用程式轉譯為 HTML。接著,用於轉譯的 JavaScript 和資料會嵌入產生的文件中。如果謹慎處理,就能像伺服器端轉譯一樣快速完成 FCP,然後在用戶端再次轉譯,完成「接續」作業。
這是有效的解決方案,但可能會大幅降低效能。
使用重新凍結的伺服器端轉譯,主要缺點是即使能改善 FCP,也可能對 TBT 和 INP 造成嚴重的負面影響。伺服器端算繪的網頁可能看起來已載入且可互動,但實際上要等到元件的用戶端指令碼執行完畢,且事件處理常式附加完成後,才能回應輸入內容。在行動裝置上,這可能需要幾分鐘的時間,讓使用者感到困惑和挫折。
復水問題:一個應用程式,兩倍價格
為了讓用戶端 JavaScript 準確接續伺服器中斷的位置,而不必重新要求伺服器轉譯 HTML 的所有資料,大多數伺服器端轉譯解決方案會將 UI 資料依附元件的回應序列化,做為文件中的指令碼標記。由於這會複製大量 HTML,因此重新凍結可能會導致更多問題,而不只是延遲互動。
伺服器會傳回應用程式 UI 的說明,以回應導覽要求,但也會傳回用於組成該 UI 的來源資料,以及 UI 實作的完整副本,然後在用戶端上啟動。bundle.js 載入並執行完畢後,使用者才能與 UI 互動。
從使用伺服器端轉譯和復原的實際網站收集的成效指標顯示,這很少是最佳選項。最主要的原因是,如果網頁看起來已載入完成,但互動功能都無法運作,會影響使用者體驗。
伺服器端轉譯搭配 Rehydration 則有機會解決這個問題。短期內,如果只對可快取的內容使用伺服器端算繪,可以縮短 TTFB,產生與預先算繪類似的結果。漸進式、逐步或部分重新凍結,可能是未來讓這項技術更可行的關鍵。
串流伺服器端轉譯及逐步重新凍結
過去幾年來,伺服器端算繪技術已有許多發展。
串流伺服器端轉譯可讓您以區塊形式傳送 HTML,瀏覽器在收到 HTML 時可逐步轉譯。這樣一來,使用者就能更快取得標記,進而加快 FCP。在 React 中,renderToPipeableStream() 中的串流是非同步,而 renderToString() 是同步,這表示背壓處理得很好。
漸進式重新凍結也值得考慮 (React 已實作)。這種做法會隨著時間「啟動」伺服器轉譯應用程式的個別部分,而非目前常見的一次初始化整個應用程式。這有助於減少網頁互動所需的 JavaScript 量,因為您可以延遲用戶端升級網頁中低優先順序的部分,避免阻擋主執行緒,讓使用者互動在使用者啟動後更快發生。
漸進式復水也有助於避免伺服器端轉譯復水最常見的陷阱之一:伺服器轉譯的 DOM 樹狀結構遭到破壞,然後立即重建,這通常是因為初始同步用戶端轉譯所需的資料尚未準備就緒,通常是尚未解析的 Promise。
部分復原
部分復水功能難以實作。這種做法是漸進式復水的延伸,會分析頁面的個別部分 (元件、檢視畫面或樹狀結構),並找出互動性低或沒有反應的部分。對於這些大部分為靜態的部分,系統會將對應的 JavaScript 程式碼轉換為惰性參照和裝飾功能,將用戶端足跡減少到幾乎為零。
部分重新凍結的做法有其問題和缺點。這會對快取帶來一些有趣的挑戰,而用戶端導覽意味著,我們無法假設應用程式閒置部分的伺服器轉譯 HTML 可在不載入完整網頁的情況下使用。
三同構轉譯
如果可以使用 Service Worker,建議您考慮三態轉譯。這項技術可讓您在初始或非 JavaScript 導覽中使用串流伺服器端轉譯,然後讓 Service Worker 在安裝後負責轉譯 HTML。這可讓快取的元件和範本保持最新狀態,並啟用 SPA 樣式的導覽,在同一個工作階段中轉譯新檢視畫面。如果可以在伺服器、用戶端頁面和 Service Worker 之間共用相同的範本和轉送程式碼,這個做法就非常實用。
SEO 考量
選擇網頁轉譯策略時,團隊通常會考量對 SEO 的影響。伺服器端轉譯是提供「完整外觀」體驗的熱門選擇,可讓檢索器解讀。檢索器可以解讀 JavaScript,但轉譯方式通常有限制。用戶端轉譯雖然可行,但通常需要額外測試和負擔。最近,如果您的架構高度仰賴用戶端 JavaScript,動態轉譯也成為值得考慮的選項。
結論
決定轉譯方法時,請評估並瞭解瓶頸所在。請考量靜態轉譯或伺服器端轉譯是否能滿足大部分需求。您可以主要傳送 HTML,並搭配少量 JavaScript,提供互動式體驗。這張資訊圖表清楚呈現伺服器與用戶端之間的關係:
抵免額
感謝所有提供評論和靈感的人:
Jeffrey Posnick、Houssein Djirdeh、Shubhie Panicker、Chris Harrelson 和 Sebastian Markbåge。