基於 SSR 的預渲染首屏直出方案

牧云云發表於2022-01-23

基於 SSR 的預渲染首屏直出方案

Create React Doc 是一個使用 React 的 markdown 文件站點生成工具。此前在 Create React Doc 中引入了預渲染技術來預先生成對應路由的靜態頁面,以使基於其搭建的文件站點能享用到 SEO(Search Engine Optimization) 同時加快了首屏訪問載入。

新的挑戰

Create React Doc 使用預渲染技術獲取各頁面路由對應的 DOM 結構以生成對應的 HTML 檔案,並將靜態檔案存放於 gh-pages 服務中(可自行選擇其它儲存服務)從而達到加快首屏訪問載入以及 SEO。見如下藍色線框流程圖部分:

下圖為 gp-pages 服務存放的靜態目錄檔案:

在訪問 Create React Doc 建立的文件時,頁面渲染週期可分為首屏渲染階段銜接階段可互動階段

首屏渲染階段: 以訪問快速上手章節為例,當使用者在瀏覽器輸入 http://muyunyun.cn/create-react-doc/290a4219/ 時,gp-pages 服務會推送預先渲染好的頁面,此時使用者可以獲得十分快速的首屏體驗 ?。

不過需要指出的是,預渲染的頁面僅僅只是生成靜態的 HTML 頁面,因而在首屏渲染階段的頁面時使用者是無法互動的。

銜接階段: 銜接階段是首屏渲染階段頁面可互動階段的中間態階段,在該階段執行 JavaScript 邏輯,從而使頁面從不可互動到可互動。但是觀察發現從預渲染頁面到頁面可互動,出現了干擾體驗的載入頁,體驗十分不好 ?。

不被期望的中間載入頁(見上圖)出現的原因為預渲染頁面與客戶端渲染頁面都使用了 ReactDom.render 並指定相同根路徑節點(這裡為 root)進行渲染。在訪問首屏預渲染頁面之後,執行 JavaScript 邏輯時,React 會移除存量 HTML 結構,並基於 root 節點重新開始渲染,因而必然會導致出現不被期望的載入頁或者頁面抖動。

ReactDOM.render(
  <RouterRoot />,
  document.getElementById('root'),
)

可互動階段:該階段使用者可以與頁面進行互動。比如點選左側選單按鈕可以展開、收起等。

基於 SSR 的預渲染首屏直出方案

基於文件站點大部分為靜態內容,少部分為動態可互動內容。抽象出以下幾種可行性思路:

  • 思路一:調整互動佈局,減少動態節點的互動。比如使用麵包屑元件與平鋪選單結構來替換多層級選單,或者探尋更優雅的 CSS 互動方案。

  • 思路二:解耦靜態節點與動態互動節點渲染的時機。預渲染時完成大部分靜態頁面的渲染,在銜接階段中完成動態邏輯節點的執行。虛擬碼如下:

if (!ifProdRender) {
  // 預渲染靜態節點
  ReactDOM.render(
    <QuietNode />,
    document.getElementById('quietNode'),
  )
} else {
  // 銜接階段完成動態互動節點的渲染
  ReactDOM.render(
    <DynamicNode />,
    document.getElementById('dynamicNode'),
  )
}

基於上述程式碼,可實現靜態頁面節點與動態互動節點的分開渲染。但該方案的缺陷是靜態節點與動態互動節點之間的聯絡被完全割裂開,銜接階段渲染的節點不能影響到靜態頁面節點,比如頁面佈局、路由跳轉等。

  • 思路三:解耦靜態節點渲染與動態互動生效的時機,保證靜態節點與動態互動節點渲染之間的聯絡。在思路二基礎上,進一步聯想到如果基於服務端渲染(在服務端首屏直出靜態頁面,在客戶端注水互動邏輯)不就可以完美支援靜態節點與動態互動隔離執行,同時保證銜接階段頁面不出現抖動了麼。只不過我們這裡的服務端可以使用 gh-pages 服務來存放基於 SSR 提前預渲染好的節點。

根據環境執行不同的渲染邏輯的程式碼如下示意,完整改動可見 mr

if (ifDev) {
  // dev render
  document.getElementById('root').innerHTML = ReactDOMServer.renderToString(<RouterRoot />)
  ReactDOM.hydrate(
    <RouterRoot />,
    document.getElementById('root'),
  )
} else if (ifPrerender) {
  // prerender
  document.getElementById('root').innerHTML = ReactDOMServer.renderToString(<RouterRoot />)
} else {
  // prod render
  ReactDOM.hydrate(
    <RouterRoot />,
    document.getElementById('root'),
  )
}

至此在銜接階段中不友好的抖動問題(不被期望的載入頁)得以解決,使用者在訪問站點時不會再感受到由於頁面抖動帶來不友好的體驗,同時從首屏渲染頁到頁面可互動的銜接也變得更為順滑。

小結

在靜態內容為主的文件站點中,除了首屏載入速度、SEO 之外,從首屏頁面(不可互動)到可互動階段的中間銜接態的體驗也十分重要。基於 React 技術生態前提下,本文給出了基於 SSR 的預渲染首屏直出的解法以相對完美地解決了銜接態出現的頁面抖動問題。在即將到來的 React 18 中,我們可以讓節點的互動更為即時地被響應,以更進一步優化使用者訪問體驗,讓我們拭目以待吧。

相關文章