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