Rendering on the Web
作為開發人員,我們經常面臨會影響應用程式整個架構的決策。 Web 開發人員必須做出的核心決策之一是在他們的應用程式中實作邏輯和呈現的位置。 這可能很困難,因為有許多不同的方法來建構網站。
我們對這一領域的了解源于我們過去幾年在 Chrome 中與大型網站的交流工作。 從廣義上講,我們鼓勵開發人員考慮伺服器渲染或靜态渲染,而不是完全重新 hydration 的方法。
為了更好地了解我們在做出這個決定時選擇的架構,我們需要對每種方法和在談論它們時使用的一緻術語有深刻的了解。 這些方法之間的差異有助于從性能的角度說明在 Web 上渲染的權衡。
渲染
SSR:伺服器端渲染 - 在伺服器上将用戶端或通用應用程式渲染為 HTML。
CSR:用戶端渲染 - 在浏覽器中渲染應用程式,通常使用 DOM。
Rehydration:在用戶端“啟動”JavaScript 視圖,以便它們重用伺服器渲染的 HTML 的 DOM 樹和資料。
Prerendering:在建構時運作用戶端應用程式以将其初始狀态捕獲為靜态 HTML。
TTFB:Time to First Byte - 被視為單擊連結和第一個内容進入之間的時間。
FP:First Paint - 任何像素第一次對使用者可見。
FCP:First Contentful Paint - 請求的内容(文章正文等)變得可見的時間。
TTI: Time To Interactive - 頁面變為可互動的時間(事件連接配接等)。
Server Rendering
伺服器渲染為伺服器上的頁面生成完整的 HTML 以響應導航。 這避免了在用戶端上進行資料擷取和模闆化的額外往返,因為它是在浏覽器獲得響應之前處理的。
伺服器渲染通常會産生快速的首次繪制 (FP) 和首次内容繪制 (FCP)。 在伺服器上運作頁面邏輯和渲染可以避免向用戶端發送大量 JavaScript,這有助于實作快速互動時間 (TTI)。 這是有道理的,因為通過伺服器渲染,您實際上隻是向使用者的浏覽器發送文本和連結。 這種方法可以很好地适用于各種裝置和網絡條件,并開啟了有趣的浏覽器優化,例如流式文檔解析。
通過伺服器渲染,使用者不太可能在使用您的網站之前等待 CPU 綁定的 JavaScript 處理。即使無法避免第三方 JS,使用伺服器渲染來降低您自己的第一方 JS 成本也可以為您提供更多的“預算”。但是,這種方法有一個主要缺點:在伺服器上生成頁面需要時間,這通常會導緻首位元組時間 (TTFB) 變慢。
伺服器渲染是否足以滿足您的應用程式在很大程度上取決于您正在建構的體驗類型。關于伺服器渲染與用戶端渲染的正确應用程式存在長期争論,但重要的是要記住,您可以選擇對某些頁面使用伺服器渲染而不是其他頁面。一些網站已經成功地采用了混合渲染技術。 Netflix 伺服器呈現其相對靜态的登陸頁面,同時為互動密集型頁面預取 JS,為這些較重的用戶端呈現頁面提供更好的快速加載機會。
許多現代架構、庫和架構使得在用戶端和伺服器上呈現相同的應用程式成為可能。這些技術可用于伺服器渲染,但重要的是要注意渲染發生在伺服器和用戶端的架構是它們自己的解決方案類,具有非常不同的性能特征和權衡。 React 使用者可以使用 renderToString() 或基于它建構的解決方案,例如 Next.js 進行伺服器渲染。 Vue 使用者可以檢視 Vue 的伺服器渲染指南或 Nuxt。 Angular 有 Universal framework.大多數流行的解決方案都采用某種形式的 hydration 作用,是以在選擇工具之前請注意使用的方法。
Static Rendering
靜态渲染發生在建構時,并提供快速的首次繪制、首次内容繪制和互動時間 - 假設用戶端 JS 的數量有限。 與伺服器渲染不同,它還設法實作始終如一的快速首位元組時間,因為頁面的 HTML 不必即時生成。 通常,靜态呈現意味着提前為每個 URL 生成單獨的 HTML 檔案。 通過提前生成 HTML 響應,可以将靜态呈現器部署到多個 CDN 以利用邊緣緩存。
靜态渲染的解決方案有各種形狀和大小。 Gatsby 之類的工具旨在讓開發人員感覺他們的應用程式是動态呈現的,而不是作為建構步驟生成的。其他像 Jekyll 和 Metalsmith 這樣的公司擁抱它們的靜态特性,提供了一種更加模闆驅動的方法。
靜态呈現的缺點之一是必須為每個可能的 URL 生成單獨的 HTML 檔案。如果您無法提前預測這些 URL 的内容,或者對于具有大量獨特頁面的網站,這可能具有挑戰性甚至不可行。
React 使用者可能熟悉 Gatsby、Next.js 靜态導出或 Navi - 所有這些都可以友善地使用元件進行創作。但是,了解靜态渲染和預渲染之間的差別很重要:靜态渲染頁面是互動式的,無需執行大量用戶端 JS,而預渲染改進了必須啟動的單頁應用程式的首次繪制或首次内容繪制用戶端以使頁面真正具有互動性。
如果您不确定給定的解決方案是靜态渲染還是預渲染,請嘗試以下測試:禁用 JavaScript 并加載建立的網頁。對于靜态呈現的頁面,大多數功能在沒有啟用 JavaScript 的情況下仍然存在。對于預渲染頁面,可能仍然有一些基本功能,如連結,但大部分頁面将是惰性的。
另一個有用的測試是使用 Chrome DevTools 降低網絡速度,并觀察在頁面變為可互動之前已下載下傳了多少 JavaScript。預渲染通常需要更多的 JavaScript 才能獲得互動性,而且 JavaScript 往往比靜态渲染使用的漸進增強方法更複雜。
Server Rendering vs Static Rendering
伺服器渲染不是靈丹妙藥——它的動态特性會帶來大量的計算開銷成本。許多伺服器渲染解決方案不會提前重新整理,可能會延遲 TTFB 或将發送的資料加倍(例如,用戶端上的 JS 使用的内聯狀态)。在 React 中,renderToString() 可能很慢,因為它是同步的和單線程的。使伺服器渲染“正确”可能涉及為元件緩存找到或建構解決方案、管理記憶體消耗、應用記憶技術以及許多其他問題。您通常會多次處理/重建同一個應用程式 - 一次在用戶端上,一次在伺服器中。僅僅因為伺服器渲染可以使某些東西更快地顯示出來并不突然意味着您要做的工作更少。
SSR 為每個 URL 按需生成 HTML,但可能比僅提供靜态呈現的内容慢。如果您可以進行額外的工作,伺服器渲染 + HTML 緩存可以大大減少伺服器渲染時間。與靜态渲染相比,伺服器渲染的優勢在于能夠提取更多“實時”資料并響應更完整的請求集。需要個性化的頁面是不能很好地與靜态呈現一起工作的請求類型的具體示例。
在建構 PWA 時,伺服器渲染也可以提供有趣的決策。使用全頁面 Service Worker 緩存還是僅伺服器渲染單個内容更好?
Client-Side Rendering (CSR)
用戶端渲染 (CSR) 是指使用 JavaScript 直接在浏覽器中渲染頁面。 所有邏輯、資料擷取、模闆和路由都在用戶端而不是伺服器上處理。
對于移動裝置,用戶端渲染可能難以獲得并保持快速。 如果做最少的工作,保持緊張的 JavaScript 預算并以盡可能少的 RTT 傳遞價值,它可以接近純伺服器渲染的性能。 使用 HTTP/2 伺服器推送或 可以更快地傳遞關鍵腳本和資料,這可以讓解析器更快地為您工作。 像 PRPL 這樣的模式值得評估,以確定初始和後續導航感覺即時。
用戶端渲染的主要缺點是所需的 JavaScript 量會随着應用程式的增長而增長。 添加新的 JavaScript 庫、polyfill 和第三方代碼後,這變得尤其困難,它們會争奪處理能力,并且通常必須在呈現頁面内容之前進行處理。 使用依賴于大型 JavaScript 包的 CSR 建構的體驗應該考慮積極的代碼拆分,并確定延遲加載 JavaScript——“隻在需要時提供您需要的服務”。 對于互動性很少或沒有互動性的體驗,伺服器渲染可以代表這些問題的更具可擴充性的解決方案。
對于建構單頁應用程式的人來說,識别大多數頁面共享的使用者界面的核心部分意味着您可以應用應用程式外殼緩存技術。 與服務工作者相結合,這可以顯着提高重複通路時的感覺性能。
Combining server rendering and CSR via rehydration
通常被稱為 Universal 渲染或簡稱為“SSR”,這種方法試圖通過同時進行用戶端渲染和伺服器渲染之間的權衡來平衡。諸如整頁加載或重新加載之類的導航請求由将應用程式呈現為 HTML 的伺服器處理,然後用于呈現的 JavaScript 和資料被嵌入到生成的文檔中。如果仔細實施,這将實作快速的第一次内容繪制,就像伺服器渲染一樣,然後通過使用稱為(重新)Hydration 的技術在用戶端再次渲染來“拾取 Pick up”。這是一個新穎的解決方案,但它可能有一些相當大的性能缺陷。
帶有再 hydration 的 SSR 的主要缺點是它會對互動時間産生顯着的負面影響,即使它改進了 First Paint。 SSR 的頁面通常看起來像是在加載和互動,但在執行用戶端 JS 并附加事件處理程式之前,它們實際上無法響應輸入。在移動裝置上這可能需要幾秒鐘甚至幾分鐘的時間。
也許您自己也經曆過這種情況 - 在看起來頁面已加載後的一段時間内,單擊或點選什麼也沒做。這很快變得令人沮喪…“為什麼什麼都沒有發生?為什麼我不能滾動?”
A Rehydration Problem: One App for the Price of Two
Rehydration 問題通常比 JS 導緻的延遲互動更糟糕。 為了讓用戶端 JavaScript 能夠準确地“接收”伺服器中斷的位置,而不必重新請求伺服器用于呈現其 HTML 的所有資料,目前的 SSR 解決方案通常将來自 UI 的響應序列化 資料依賴項作為腳本标簽寫入文檔。 生成的 HTML 文檔包含高度重複:
如您所見,伺服器響應導航請求傳回應用程式 UI 的描述,但它也傳回用于組成該 UI 的源資料,以及 UI 實作的完整副本,然後在用戶端啟動 . 隻有在 bundle.js 完成加載和執行後,這個 UI 才會變成互動式。
從使用 SSR 再水化的真實網站收集的性能名額表明,應強烈建議不要使用它。 歸根結底,原因歸結為使用者體驗:最終很容易讓使用者陷入“恐怖谷”。
Streaming server rendering and Progressive Rehydration
在過去的幾年中,伺服器渲染有了許多發展。
流式伺服器呈現允許您以塊的形式發送 HTML,浏覽器可以在接收到時逐漸呈現這些 HTML。這可以提供快速的首次繪制和首次内容繪制,因為标記更快地到達使用者手中。在 React 中,流在 renderToNodeStream() 中是異步的——與同步 renderToString 相比——意味着 backpressure 得到了很好的處理。
漸進式 rehydration 也值得關注,這是 React 一直在探索的東西。使用這種方法,伺服器渲染的應用程式的各個部分會随着時間的推移而“啟動”,而不是目前一次初始化整個應用程式的常用方法。這有助于減少使頁面具有互動性所需的 JavaScript 量,因為可以推遲頁面低優先級部分的用戶端更新以防止阻塞主線程。它還可以幫助避免最常見的 SSR Rehydration 陷阱之一,其中伺服器渲染的 DOM 樹被破壞然後立即重建——通常是因為初始同步用戶端渲染需要尚未準備好的資料,可能正在等待 Promise解析度。
SEO Considerations
在選擇在 Web 上呈現的政策時,團隊通常會考慮 SEO 的影響。 通常選擇伺服器渲染來提供爬蟲可以輕松解釋的“完整外觀”體驗。 爬蟲可能了解 JavaScript,但在它們的呈現方式中通常有一些值得關注的局限性。 用戶端渲染可以工作,但通常需要額外的測試和工作。 如果您的架構很大程度上由用戶端 JavaScript 驅動,那麼最近動态渲染也成為一個值得考慮的選項。
如有疑問,Mobile Friendly 測試工具對于測試您選擇的方法是否符合您的期望非常有用。 它顯示了任何頁面在 Google 抓取工具中的顯示方式、找到的序列化 HTML 内容(在執行 JavaScript 之後)以及呈現期間遇到的任何錯誤的視覺預覽。
總結
在決定渲染方法時,測量并了解您的瓶頸是什麼。 考慮靜态渲染或伺服器渲染是否可以讓您完成 90% 的工作。 使用最少的 JS 來主要釋出 HTML 以獲得互動體驗是完全可以的。 這是一個友善的資訊圖,顯示了伺服器 - 用戶端範圍: