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

基於 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 中,我們可以讓節點的交互更爲即時地被響應,以更進一步優化用戶訪問體驗,讓我們拭目以待吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章