Web渲染那些事兒

做爲開發者,常常須要面對影響整個應用架構的決策。而Web開發者的核心決策之一,就是應用邏輯與渲染工做的實現,應處於架構中的什麼位置(譯註:客戶端 or 服務器?)。如今有不少不一樣構建網站的方法,所以這些決策變得越發困難。javascript

咱們對這一領域的理解,來自於咱們過去幾年在 Chrome 工做中,與大型網站的交流。從廣義上講,咱們鼓勵開發人員考慮經過一種稱爲 rehydration 的方式,進行服務器渲染或靜態渲染。html

爲了更好地理解在作出決定時所選擇的架構,咱們須要對每種方法有充分的理解,而且在談到它們時使用一致的術語。vue

術語

渲染java

  • SSR:服務器渲染(Server-Side Rendering)——在服務器上將客戶端或通用(universal)應用程序渲染成HTML。
  • CSR:客戶端渲染(Client-Side Rendering)——在瀏覽器中渲染App,一般使用DOM。
  • Rehydration:在客戶端上「啓動」 JavaScript 視圖,複用服務器渲染的HTML DOM樹和數據。(譯註:利用服務器返回HTML中的JS數據,從新渲染頁面的技術,詳見知乎討論,其中《三體》的部分很形象~)
  • 預渲染(Prerendering):在構建時運行客戶端應用程序,以將其初始狀態捕獲爲靜態HTML。

性能node

  • TTFB:首字節時間(Time to First Byte)——從點擊連接 到 接收第一個字節內容 之間的時間。
  • FP:首次繪製(First Pain)——第一次有像素對用戶可見的時間。
  • FCP:首次內容繪製(First Contentful Paint)——請求內容(文章正文等)變得可見的時間。
  • TTI:可交互時間(Time To Interactive)——頁面變爲可交互的時間(事件綁定等)。

服務器渲染(Server Rendering)

服務器渲染,指在服務器中生成整個頁面的HTML,以此響應請求的技術。這樣作避免了在客戶端上進行數據獲取的額外往返(round-trips)和模板處理,由於這些工做在瀏覽器得到響應以前,已由服務器處理了。react

服務器渲染一般會獲得快速的首次繪製(FP)和首次內容繪製(FCP)。在服務器上運行頁面邏輯和渲染,能夠避免向客戶端發送大量 JavaScript,有助於實現快速的可交互時間(TTI)。這之因此行得通,由於服務器渲染的本質,只是向用戶瀏覽器發送文本和連接。這種方法適用於普遍的設備和網絡,並能觸發一些有趣的瀏覽器優化,好比流文檔解析。git

圖片描述

使用服務器渲染,用戶再也不須要在客戶端上等待 CPU 相關的 JavaScript 處理後,而後才能訪問站點。即便第三方JS沒法避免,使用服務器渲染來減小本身的JS成本,也能提供更多的性能「預算」。可是,這種方法有一個主要缺點:在服務器上生成頁面有必定耗時,可能會致使較慢的首字節時間(TTFB)。github

服務器渲染是否知足應用程序,很大程度上取決於構建目標的體驗類型。關於服務器渲染與客戶端渲染的正確應用存在長期爭論,但重要的是咱們能夠選擇對某些頁面使用服務器渲染,而對其他頁面不使用。一些網站已成功採用混合渲染技術:Netflix 服務器渲染其相對靜態的落地頁面,同時爲交互繁重的頁面預拉取JS,爲這些重客戶端頁面提供更快的加載能力。web

許多現代框架、庫和架構,使得在客戶端和服務器上渲染相同的應用程序成爲可能。這些技術可用於服務器渲染,可是要注意,在服務器和客戶端上進行渲染的架構,都是各框架自家的解決方案,具備不一樣的性能特色和權衡。React 用戶可使用 renderToString() 或在其上構建的解決方案如 Next.js,用於服務器渲染;Vue 用戶能夠查看 Vue 的服務器渲染指南Nuxt;Angular 有 Universal。大部分流行的解決方案採用某種 hydration 的形態,所以在選擇工具以前要注意使用的方法。shell

靜態渲染(Static Rendering)

靜態渲染在構建時進行,並提供快速的 FP、FCP 和 TTI——假設客戶端JS的體積得當。與服務器渲染不一樣,它還致力於實現始終如一的快速首字節時間(TTFB),由於頁面的 HTML 沒必要動態生成。一般,靜態渲染意味着提早爲每一個 URL 生成單獨的 HTML 文件。經過預先生成 HTML 響應,能夠將靜態渲染部署到多個 CDN 以利用邊緣緩存。(譯註:也就是「頁面靜態化」)

圖片描述

靜態渲染的解決方案選擇不少,像 Gatsby 這樣的工具旨在讓開發人員感受他們的應用程序是動態渲染的,而不是構建過程生成的。JekylMetalsmith 提供更多模板驅動的方法,更加符合它們的靜態特質。

靜態渲染的一個缺點是必須爲每一個可能的 URL 生成單獨的 HTML 文件。 若是沒法提早預測這些 URL 的內容,或者對於具備大量不一樣頁面的網站,這可能具備挑戰性甚至是不可行的。

React 用戶可能熟悉 GatsbyNext.js 靜態導出Navi ——它們均可以方便使用組件。可是,瞭解靜態渲染和預渲染之間的區別很是重要:靜態渲染頁面是無需執行太多客戶端 JS 就可交互的,預渲染則改進了單頁面應用的 FP 或 FCP,因爲是單頁面應用,因此必須等待客戶端啓動過程,以使頁面真正具備交互性。(譯註:簡單的說靜態渲染不依賴客戶端JS,適用於靜態頁面,而預渲染則依賴JS,更可能是爲了富應用的初始界面加速)

若是不肯定選擇靜態渲染仍是預渲染方案,請嘗試此測試:禁用JavaScript並加載建立的網頁。對於靜態渲染的頁面,大多數功能在未啓用JavaScript下仍然正常運做。而對於預渲染頁面,一些基本功能(如連接)能正常展示,但頁面其他部分沒法正常展示。

另外一個有效的測試是使用 Chrome DevTools 減慢網絡速度,並觀察在頁面變爲可交互以前已下載了多少 JavaScript。預渲染一般須要更多的 JavaScript 來實現交互,而且這些 JS 每每比靜態渲染使用的漸進加強方法更復雜。

服務器渲染 vs 靜態渲染

服務器渲染並非銀彈——它的動態特性帶來顯著的計算成本。許多服務器渲染解決方案會有耗時,致使延遲的 TTFB 或成倍的數據傳輸(例如,客戶端 JS 所需的內聯狀態)。在 React 中,renderToString() 可能很慢,由於它是同步和單線程的。服務器渲染「正確」的姿式,可能涉及查找或構建組件緩存方案、內存消耗管理、應用記憶化技術以及許多其餘方面。同一個應用程序一般須要屢次處理/重建——一次在客戶端中,一次在服務器中。所以服務器渲染可使某些東西更快地顯示出來,但並不意味着能夠減小工做量。

服務器渲染爲每一個 URL 按需生成 HTML,但速度可能比僅提供靜態渲染內容要慢。若是加以進行額外的工做,服務器渲染 + HTML緩存,能夠大大減小服務器渲染時間。服務器渲染的優點在於,可以提取更多「實時」數據,並響應比靜態渲染更完整的請求集。個性化頁面就是一個不適用於靜態渲染的頁面類型表明。

在構建 PWA 時,服務器渲染也拋出一個有趣的問題。 整個頁面使用 Service Worker 緩存,與服務器渲染部份內容片斷,哪一個方案更好?

客戶端渲染(Client-Side Rendering,CSR)

客戶端渲染(CSR)意味着使用 JavaScript 直接在瀏覽器中渲染頁面。 全部邏輯、數據獲取、模板和路由都在客戶端處理,而不是服務器上。

客戶端渲染很難在移動端作到很快。若是作好壓縮工做,嚴格控制 JavaScript 預算,並在儘量少的 RTT 中提供內容,它能夠接近純服務器渲染的性能。使用 HTTP/2 Server Push 或 <link rel = preload> 能夠更快地提供關鍵腳本和數據,這將使解析器更快地完成工做。像 PRPL 這樣的模式值得評估,以確保初始和後續導航的即時感。

圖片描述

客戶端渲染的主要缺點是,隨着應用程序的發展,所需的 JavaScript 數量會增長。隨着添加新的 JavaScript 庫、polyfill 和第三方代碼,更是一發不可收拾。這些代碼會競爭處理能力,而且一般必須在渲染頁面內容以前完成處理。構建依賴大型 JavaScript 的 CSR 應用時,應該考慮積極的代碼分割,並確保延遲加載 JavaScript——「只在須要時提供所需內容」。對於不多或沒有交互性的頁面,服務器渲染能夠做爲更具擴展性的解決方案。

對於構建單頁應用程序的人來講,識別大多數頁面共享的UI核心部分,意味着能夠應用 Application Shell 緩存技術。與 Service Worker 相結合,能夠顯著提升重複訪問的感知性能。

經過 Rehydration 將服務器渲染和 CSR 相結合

這種方法一般被稱爲通用渲染或簡稱爲「SSR」,它試圖經過二者兼顧來平滑客戶端渲染和服務器渲染之間的權衡。頁面請求交由服務器處理,將應用程序渲染爲 HTML,而後把用於渲染的 JavaScript 和數據,嵌入到生成的文檔中。只要處理得當,這就像服務器渲染同樣實現了快速的 FCP,而後經過稱爲 (re)hydration 的技術,在客戶端上再次「拾取」來渲染。這是一種新穎的解決方案,但也具備一些明顯性能缺陷。
譯註:若是這裏很差理解,請先理解上面術語部分中 Rehydration 的知乎連接內容。

rehydration 後的 SSR 主要缺點,是它會對可交互時間(TTI)產生顯著的負面影響,即便它改善了首次繪製(FP)。SSR 頁面一般看起來具備欺騙性的加載完成和可交互性,但在執行客戶端JS並綁定事件處理以前,頁面實際上沒法響應輸入。這在移動設備上可能持續幾秒甚至幾分鐘。

也許你本身也經歷過這種狀況——在頁面看起來已經加載後的一段時間內,點擊或觸摸什麼都沒反應。這很快變得使人沮喪......「爲何沒有反應? 爲何我不能滾動?「

一個 Rehydration 問題:應用的雙重成本

因爲JS特性,Rehydration 問題每每比延遲交互更糟糕。爲了使客戶端 JavaScript 可以不用從新請求服務器,就能準確地獲取服務器返回的用於呈現其 HTML 的全部數據,當前的 SSR 解決方案一般將UI的數據響應序列化, 以 Script 標籤形式存放在 HTML 中。結果是生成的 HTML 文檔包含大量重複片斷:

圖片描述

正如你所看到的,服務器除了返回應用程序 UI 以響應頁面請求,還返回了用於組成該 UI 的源數據,以及生成相同 UI 的實現代碼,即刻在客戶端上運行。只有在 bundle.js 完成加載和執行後,頁面纔會變爲可交互。

從使用 Rehydration SSR 站點收集的性能數據顯示,這種用法應極力避免。歸根結底,緣由歸結爲用戶體驗:很容易讓用戶處於「不明因此」的狀態。

圖片描述

Rehydration SSR 也不是沒有但願。在短時間內,僅將 SSR 用於高度可緩存的內容,能夠減小 TTFB 延遲,從而達到與預渲染相似的結果。

流式服務器渲染和漸進式 Rehydration

服務器渲染在過去幾年中發展迅猛。

流式服務器渲染能以 chunk 形式發送 HTML,瀏覽器能夠在接收時逐塊渲染。這促成了快速的 First Paint 和 First Contentful Paint,由於 HTML 標籤更快地到達用戶側。在 React 中,流在 renderToNodeStream() 中異步處理,相比於同步的 renderToString,服務器的壓力也會更小。

漸進式 Rehydration 也值得關注,React 一直在探索。使用這種方法,服務器渲染後的頁面各部分,隨着時間推移被「啓動」,而不是一般一次初始化整個應用程序的作法。這能夠減小頁面可交互所需的 JavaScript 量,由於能夠延遲頁面低優先級部分,以防止阻塞主線程。它還能夠幫助避免最多見的 SSR Rehydration 陷阱:服務器渲染的DOM樹被破壞後當即重建——一般是由於客戶端初始同步渲染所需的數據還沒準備好,好比還在等待 Promise 的解析。

部分 Rehydration

部分 Rehydration 已被證實難以實現。該方法是漸進式 Rehydration 概念的擴展,經過分析漸進式 Rehydration 的各個部分(組件/視圖/樹),識別出那些不具交互性的部分。對於每一個基本靜態的部分,相應的 JavaScript 代碼會被轉換爲惰性引用和裝飾功能,將其客戶端佔用空間減小到接近於零。部分 Rehydration 方案伴隨着自身的問題和妥協。它爲緩存帶來了一些有趣的挑戰,咱們沒法假設服務器渲染的惰性部分 HTML,在頁面完整加載前是可用的。

三方同構渲染(Trisomorphic Rendering)

若是可使用 service worker,「trisomorphic」渲染也頗有意思。該技術是指,利用流式服務器渲染初始頁面,等 Service Worker 加載後,接管 HTML 的渲染工做。這可使緩存的組件和模板保持最新,並啓用 SPA 式的導航以在同一會話中渲染新視圖。當能夠在服務器、客戶端頁面和 Service Worker 之間共享相同模板和路由代碼時,此方法最有效。
圖片描述

SEO 考慮

在選擇渲染策略時,團隊一般會考慮 SEO 的影響。爲了讓爬蟲可以輕鬆得到「完整頁面」,服務器渲染是不二的選擇。雖然爬蟲可能會理解 JavaScript,可是在渲染方式上的侷限性須要注意。若是你的應用很是重 JavaScript,最近的動態渲染方案也是個值得考慮的選擇。

若是有疑問,Mobile-Friendly Test 工具對於測試你選擇的方法是否符合預期,很是有用。它展現了 Google 爬蟲渲染頁面的預覽、序列化的 HTML 內容(執行 JavaScript 後),以及渲染過程當中發生的錯誤。
圖片描述

總結

在決定渲染方式時,須要測量和理解真正的瓶頸在哪裏。靜態渲染或服務器渲染在多數狀況都比較適用,尤爲是可交互性對JS依賴較低的場景。下面是一張便捷的信息圖,顯示了服務器到客戶端的技術頻譜:
圖片描述

相關文章
相關標籤/搜索