網頁開發人員必須決定 在應用程式中實作邏輯和算繪的一點是核心決策環境並不容易,因為建立網站的方法有很多種
過去幾年來,我們在 Chrome 與大型網站討論的成果,正是我們對這個空間的理解。廣泛來說,我們鼓勵開發人員考慮透過完整的補水方法,考慮伺服器端算繪或靜態算繪作業。
為了深入瞭解我們做出這項決策時選擇的架構,我們需要對每種方法有充分的瞭解,並在討論時採用一致的術語。從轉譯方式之間的差異有助於瞭解網頁算繪與網頁效能之間的取捨。
術語
轉譯
- 伺服器端轉譯 (SSR)
- 在伺服器上將用戶端或通用應用程式算繪為 HTML。
- 用戶端轉譯 (CSR)
- 使用 JavaScript 在瀏覽器中轉譯應用程式,並利用 JavaScript 修改 DOM。
- 補充水分
- 在用戶端上「啟動」JavaScript 檢視畫面,以便重複使用伺服器算繪 HTML 的 DOM 樹狀結構和資料。
- 預先算繪
- 在建構期間執行用戶端應用程式,以擷取靜態 HTML 的初始狀態。
效能
- 首次位元組時間 (TTFB)
- 從點選連結到在新頁面載入內容第一個位元組之間的時間。
- 首次顯示內容所需時間 (FCP)
- 顯示要求內容 (文章內文等) 的時間。
- 與下一個顯示的內容 (INP) 互動
- 這個代表性指標可用於評估網頁是否持續快速回應使用者輸入內容。
- 總封鎖時間 (TBT)
- INP 的 Proxy 指標,計算網頁載入期間主要執行緒遭到封鎖的時間長度。
伺服器端算繪
伺服器端轉譯作業會為伺服器中的網頁產生完整的 HTML,以回應導覽。這樣可以避免在用戶端上擷取資料及建立範本所需的來回行程,因為轉譯器會在瀏覽器收到回應前進行處理。
伺服器端算繪通常會產生快速的 FCP。在伺服器上執行頁面邏輯並進行轉譯,可避免將大量 JavaScript 傳送至用戶端。這有助於減少網頁的 TBT,進而導致 INP 較低,因為在頁面載入期間,主要執行緒沒有經常遭到封鎖。如果主執行緒的封鎖頻率較低,使用者互動的機會就越多。這是很合理的,因為在伺服器端轉譯,您只需要傳送文字和連結給使用者的瀏覽器。此方法適用於各種裝置和網路狀況,並可開啟有趣的瀏覽器最佳化功能,例如串流文件剖析。
如果採用伺服器端轉譯,使用者在使用您的網站前,就不太可能等待受 CPU 限制的 JavaScript 執行。即使您無法避免避免使用第三方 JS,使用伺服器端轉譯功能來減少自己的第一方 JavaScript 費用,可以提供更多預算。不過,這種方法有一項潛在的取捨:在伺服器上產生頁面需要時間,這可能會增加網頁的 TTFB。
至於伺服器端轉譯作業是否足以滿足應用程式的需求,主要取決於要建構的使用體驗。對於伺服器端轉譯與用戶端轉譯的正確應用程式,仍有長期的討論,但您仍然可以選擇針對某些網頁啟用伺服器端轉譯,而不對其他網頁採用。有些網站成功採用混合式轉譯技術。舉例來說,Netflix 伺服器轉譯相對靜態的到達網頁,同時針對大量互動網頁預先擷取 JS,因此較能快速載入用戶端轉譯的 JS。
許多現代的架構、程式庫和架構都可讓您在用戶端和伺服器上轉譯相同的應用程式。您可以運用這些技術進行伺服器端轉譯。但是,在伺服器「和」用戶端上進行轉譯的架構,都有其專屬的解決方案類別,其效能特性和優缺點截然不同。React 使用者可以使用伺服器 DOM API,或以 Next.js 等為基礎建構的解決方案進行伺服器端轉譯。Vue 使用者可以參閱 Vue 的伺服器端轉譯指南或 Nuxt。Angular 提供「Universal」(通用)。不過,最常見的解決方案採用某種補水方式,因此請留意您的工具使用的方法。
靜態算繪
建構期間會發生靜態轉譯作業。這個方法提供快速的 FCP,以及較低的 TBT 和 INP,只要限制網頁上的用戶端 JS 數量即可。與伺服器端算繪不同,網頁的 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 支援串流功能,這類 API 可以在伺服器產生 HTML 回應的初始部分,更快取得瀏覽器的初始部分。
使伺服器端轉譯作業「正確」可能涉及尋找或建構元件快取的解決方案、管理記憶體用量、使用記憶技術,以及其他問題。您通常會在用戶端和伺服器上一次處理或重新建構相同的應用程式。較快的伺服器端算繪作業不一定能減輕您的負擔。如果您在用戶端產生了伺服器產生的 HTML 回應之後,對用戶端執行許多工作,這仍可能為網站帶來更高的 TBT 和 INP。
伺服器端算繪作業會為每個網址產生隨選 HTML,但可能比提供靜態轉譯內容的速度慢。如果能額外加裝,伺服器端算繪和 HTML 快取功能可能會大幅縮短伺服器轉譯時間。在伺服器端算繪之外,與靜態轉譯相比,能夠提取更多「即時」資料,並回應更完整的要求組合。需要個人化的網頁是具體的範例,不適用於靜態轉譯。
建構 PWA 時,伺服器端轉譯功能也能帶來有趣的決策:使用全頁服務工作站快取,或只使用伺服器轉譯個別內容片段是更好的做法嗎?
用戶端轉譯
用戶端轉譯是指使用 JavaScript 直接在瀏覽器中轉譯網頁。所有邏輯、資料擷取、範本和轉送都會在用戶端 (而不是伺服器上) 處理。有效的結果是將更多資料從伺服器傳遞至使用者的裝置,進而產生自己的取捨。
針對行動裝置,用戶端算繪並不容易,而且維持快速。只要多點工作就能精準管理 JavaScript 預算,並盡可能減少往返作業的價值,進而實現用戶端轉譯,幾乎複製純伺服器端轉譯的效能。您可以利用 <link rel=preload>
提供重要指令碼和資料,讓剖析器更快代您處理工作。此外,我們也建議使用 PRPL 等模式,確保初始和後續導覽操作都能順暢運作。
而用戶端轉譯的主要缺點是,隨著應用程式增長,所需的 JavaScript 數量通常會隨之增加,這可能會影響網頁的 INP。加入新的 JavaScript 程式庫、polyfill 和第三方程式碼會競逐處理能力,而且通常在可以轉譯頁面內容之前便需要處理,因此會變得特別困難。
如果體驗使用用戶端轉譯並仰賴大型 JavaScript 套件,則應考慮採用積極的程式碼分割機制,在頁面載入期間降低 TBT 和 INP,以及延遲載入 JavaScript,只在需要時滿足使用者需求。對於互動性很少或沒有互動的情況,伺服器端轉譯代表這些問題的擴充性較高。
對於建構單頁應用程式的人而言,識別大部分頁面共用的使用者介面核心部分,可讓您套用應用程式殼層快取技術。結合 Service Worker 後,即可大幅提高重複造訪的感知效能,因為頁面可以快速從 CacheStorage
載入其應用程式殼層 HTML 和依附元件。
脫水結合了伺服器端和用戶端轉譯
「補充」方法是嘗試透過同時執行這兩項操作,在用戶端和伺服器端轉譯之間取得平衡。載入或重新載入頁面等導覽要求是由伺服器處理,會將應用程式算繪成 HTML,然後將用於轉譯的 JavaScript 和資料嵌入至產生的文件中。仔細觀察後,系統會執行類似伺服器端轉譯的快速 FCP,然後在用戶端再次算繪,藉此「加速」並「加速」。這個方法雖然有效,但會大幅影響效能。
使用補水進行伺服器端轉譯的主要缺點是,即使可以改善 FCP,仍可能會對 TBT 和 INP 產生重大負面影響。伺服器端轉譯的網頁似乎可以載入及互動,但要等到元件的用戶端指令碼執行並附加事件處理常式後,才能實際回應輸入內容。在行動裝置上,這可能需要數分鐘、令人困惑又不悅使用者。
補水問題:一個價格為二的應用程式
為了讓用戶端 JavaScript 準確地「接續」伺服器進度,而不重新要求伺服器轉譯其 HTML 的所有資料,大多數的伺服器端轉譯解決方案都會將 UI 資料依附元件的回應序列化為文件中的指令碼標記。因為這樣會複製大量的 HTML,因此脫水會造成更多問題,不僅僅是延遲互動。
伺服器會傳回應用程式 UI 的說明,以回應導覽要求,但也會傳回構成 UI 時使用的來源資料,以及 UI 實作的完整副本,接著在用戶端上啟動。待 bundle.js
完成載入和執行作業後,UI 才會設為互動式。
透過伺服器端轉譯和補充處理從實際網站收集的效能指標,也指出它很少是最佳選項。最重要的原因是,網頁在頁面就緒,但沒有任何互動功能正常運作時,對使用者體驗的影響。
不過,我們仍希望在伺服器端進行補充處理。短期內,僅針對高可快取的內容使用伺服器端算繪功能可以減少 TTTFB,產生與預先算繪類似的結果。如果想讓這項技術日後更容易運作,關鍵就在於遞增或逐步補充水分。
串流伺服器端算繪和補充水分
近幾年來,伺服器端算繪有許多開發工作。
串流伺服器端轉譯可讓您透過區塊傳送 HTML,讓瀏覽器在收到時逐步轉譯。這麼做可以更快為您的使用者加上標記,加快 FCP 的速度。在 React 中,與同步 renderToString()
相比,在 renderToPipeableStream()
中非同步的串流作業代表能妥善處理背壓。
此外,您也可以考慮循序漸進的補充水分,而 React 已導入了這項功能。透過這種方式,伺服器算繪應用程式的個別部分會隨著時間「啟動」,而不是當下一次初始化整個應用程式的常見做法。這有助於減少網頁互動所需的 JavaScript 數量,因為這項功能可讓您延遲用戶端升級網頁的低優先順序部分,以免封鎖主執行緒,讓使用者在啟動後更快發生互動。
漸進式補充水也有助於避免發生在伺服器端轉譯修正問題之一,因為由伺服器轉譯的 DOM 樹狀結構會遭到刪除並立即重建,這通常是因為初始同步的用戶端轉譯需要尚未準備就緒的資料,而 Promise
通常尚未解析。
部分補水
事實證明,部分分水已難以實施。這是一種循序漸進的補充水,可分析網頁的個別部分 (元件、檢視畫面或樹狀結構),並找出互動次數較少或沒有重複使用的部分。接著,針對這些大部分都是靜態的部分,對應的 JavaScript 程式碼會轉換成間接參照和裝飾功能,進而減少將用戶端佔用至幾乎零的用戶端空間。
部分水份攝取量其實有自己的問題和入侵。這在快取方面帶來一些有趣的挑戰,而用戶端導覽也意味著,在不載入完整頁面的情況下,我們無法假設伺服器轉譯的 HTML 用於應用程式的插入部分。
三角性算繪
如果您採用服務工作處理程序,請考慮採用三態性算繪。這項技術可讓您針對初始或非 JavaScript 導覽使用串流伺服器端算繪,然後讓服務工作工作站在安裝 HTML 後處理 HTML 進行導覽。這可讓快取元件和範本保持在最新狀態,並啟用 SPA 樣式導覽功能,以便在同一工作階段中算繪新的檢視畫面。當伺服器、用戶端頁面與服務工作站之間共用相同的範本和轉送程式碼時,這個方法就非常實用。
搜尋引擎最佳化 (SEO) 注意事項
選擇網頁轉譯策略時,團隊通常會考量 SEO 的影響。伺服器端轉譯是讓檢索器能夠解讀「完整」體驗的熱門選擇。檢索器可以解讀 JavaScript,但轉譯方式經常有一些限制。用戶端轉譯雖然可以運作,但通常需要額外的測試和負擔。近來不久,動態轉譯也是一項值得考慮的選項,如果您的架構大量仰賴用戶端 JavaScript。
如有疑問,建議您使用行動裝置相容性測試工具,測試所選做法是否符合您的需求。它會以視覺化方式預覽 Google 檢索器對任何網頁顯示的樣子、在 JavaScript 執行後找到的序列化 HTML 內容,以及轉譯期間發生的任何錯誤。
結論
在決定算繪方式時,請評估並瞭解您的瓶頸。請思考靜態轉譯或伺服器端轉譯能否為您帶來最大的助益。在大部分的情況下,僅透過極少的 JavaScript 傳送 HTML 就能獲得互動體驗。以下提供一個便利的資訊圖表 顯示伺服器用戶端的頻譜:
抵免額
感謝大家提供的評論和靈感:
Jeffrey Posnick、 Houssein Djirdeh、 Shubhie Panicker、Chris Harrelson 和 Sebastian Markb 時顯示