React 18 中新的 Suspense SSR 架構

概述

React 18 將包括對 React 服務器端渲染(SSR)性能的架構改進。這些改進是實質性的,而且是幾年來工做的結晶。這些改進大可能是在幕後進行的,但有一些選擇性機制你須要注意,特別是若是你不使用框架的話。html

主要的新 API 是  pipeToNodeWritable,你能夠在 Upgrading to React 18 on the Server 中瞭解到。咱們計劃在細節上作更多的實現,由於這不是最終版本,而且還有一些事情須要解決。前端

現有的主要的 API 是  <Suspense>.react

本文是對新的架構以及它的設計和所解決的問題的簡單概述。android

簡而言之

服務器端渲染(在這篇文章中縮寫爲 「SSR」)讓你能在服務器上將 React 組件生成 HTML,並將該 HTML 發送給你的用戶。SSR 能讓你的用戶在你的 JavaScript 包加載和運行以前看到頁面的內容。ios

React 中的 SSR 老是分幾個步驟進行:git

  • 在服務器上獲取整個應用的數據。
  • 而後在服務器上將整個應用程序渲染成 HTML 並在響應中返回。
  • 而後在客戶端加載整個應用程序的 JavaScript 代碼。
  • 而後在客戶端將 JavaScript 邏輯綁定到服務器爲整個應用程序生成的 HTML(這個過程叫 「hydration」)。

關鍵在於,每一步都必須在下一步開始以前一次性完成整個應用程序的工做。若是你的應用程序的某些部分比其餘部分慢,這樣作的效率不高。這也是幾乎全部具備必定規模的應用面臨的問題。github

React 18 讓你使用  <Suspense>  來將你的應用程序分解成較小的獨立單元。這些單元將獨立完成這些步驟,而且不會阻礙應用程序的其餘部分。所以,你的應用程序的用戶將更快地看到內容,並能更快地開始與應用程序交互。你的應用程序中最慢的部分不會拖累那些較快的部分。這些優化是自動的。你不須要寫任何特殊的代碼來實現這個功能。redis

這也意味着 React.lazy 如今能夠和 SSR 一塊兒 「正常工做」。這裏有一個 demo.數據庫

(若是你不使用框架,你將須要改變 HTML 生成的具體方式 wired up。)編程

什麼是 SSR?

當用戶加載你的應用程序時,你但願儘快展現一個徹底可交互的頁面:

這幅插圖用綠色來表達頁面的可交互的部分。換句話說,它們全部的 JavaScript 事件處理程序都已經綁定好了,點擊按鈕能夠更新狀態等等。

然而,在頁面的 JavaScript 代碼徹底加載以前,該頁面是不能交互的。這包括 React 自己和你的應用程序代碼。對於具備必定規模的應用程序,大部分的加載時間將用於下載你的應用程序代碼。

若是你不使用 SSR,用戶在 JavaScript 加載時看到的惟一東西就是一個空白的頁面。

這不是很好,這就是爲何咱們建議使用 SSR。SSR 讓你在服務器上把你的 React 組件渲染成 HTML 併發送給用戶。HTML 的交互性不強(除了簡單的內置網絡交互,如連接和表單輸入)。可是,它能讓用戶在 JavaScript 仍在加載時看到一些東西

這裏,屏幕中灰色部分表明尚未徹底可交互的部分。你的應用程序的 JavaScript 代碼尚未加載完成,因此點擊按鈕是沒有任何響應的。但特別是對於內容繁雜的網站,SSR 很是有用,由於它可讓網絡鏈接較差的用戶在 JavaScript 加載時開始閱讀或查看內容。

當 React 和你的應用代碼都在加載時,你要讓這個 HTML 是可交互的。你告訴 React:「這是在服務器上生成這個 HTML 的 App 組件。將事件處理程序綁定到該 HTML 上!」 React 會在內存中渲染你的組件樹,但不是爲其生成 DOM 節點,而是將全部邏輯綁定到現有的 HTML 上。

這個渲染組件和綁定事件處理程序的過程被稱爲 「hydration」。(這就像是用事件處理程序看成 「水」 來澆灌 「乾燥」 的 HTML。至少,我是這樣向本身解釋這個術語的。)

hydration 以後,就是 「React 正常操做」:你的組件能夠設置狀態,響應點擊等等:

你能夠看到 SSR 有點像 「魔術」。它不能使你的應用程序更快地徹底可交互。相反,它讓你更快地展現你的應用程序的非交互式版本,以便用戶在等待 JS 加載時能夠查看靜態內容。然而,這一招對於網絡鏈接不順暢的人來講有很大的不一樣,並且提升了總體的感知性能。它還有助於你的搜索引擎排名,既是由於有更容易的索引,也是由於有更快的響應速度。

注意: 不要將 SSR 與服務器組件混淆。服務器組件是一個更具實驗性的功能,目前仍在研究中,而且可能不會成爲 React 18 最第一版本的一部分。你從這裏能夠了解服務器組件。服務器組件是對 SSR 的補充,並將成爲數據獲取的推薦方式之一,但這篇文章並不介紹它們。

今天 SSR 有哪些問題?

上述方法是可行的,但在許多方面,它並非最佳的。

在展現任何東西以前,必須先獲取全部東西

現在 SSR 的一個問題是,它不容許組件 「等待數據」。在目前的 API 中,當你渲染到 HTML 時,你必須已經在服務器上爲你的組件準備好全部的數據。這意味着你必須在服務器上收集全部的數據,而後才能開始向客戶端發送任何 HTML。這樣是很低效的。

例如,假設你想渲染一個帶有評論的帖子。儘早顯示評論是很重要的,因此你要在服務器的 HTML 輸出中包括它們。但你的數據庫或 API 層很慢,這是你沒法控制的。如今,你必須作出一些艱難的選擇。若是你把它們從服務器輸出中排除,在 JS 加載完畢以前,用戶就不會看到它們。但若是你把它們包含在服務器輸出中,你就必須推遲發送其他的 HTML(例如,導航欄、側邊欄,甚至是文章內容),直到評論加載完畢,你才能渲染完整的組件樹。這樣並很差。

順便提一下,一些數據獲取方案會反覆嘗試將樹渲染成 HTML 並丟棄結果,直到數據被解決。由於 React 沒有提供更符合人體工程學的選項。咱們想提供一個不須要如此極端妥協的解決方案。

你必須先裝好全部的東西,而後才能對任何東西進行 hydration

在你的 JavaScript 代碼加載後,你會告訴 React 將 HTML 「hydrate」 並使其具備交互性。 React 在渲染你的組件時將 「走」 過服務器生成的 HTML,並將事件處理程序綁定到該 HTML 上。爲了使其發揮做用,你的組件在瀏覽器中生成的樹必須與服務器生成的樹相匹配。不然 React 就不能 「匹配它們!」 這樣作的一個很是不幸的後果是,你必須在客戶端加載全部組件的 JavaScript,才能開始對任何組件進行 hydration

例如,假設評論小組件包含不少複雜的交互邏輯,而且須要花費一些時間爲其加載 JavaScript。 如今你不得再也不次作出艱難的選擇。把服務器上的評論渲染成 HTML,以便儘早顯示給用戶,這是一個好辦法。可是,因爲現在的 hydration 只能一次完成,因此在加載評論小組件的代碼以前,你不能開始 hydrate 導航欄、側邊欄和文章內容。固然,你可使用代碼分割並單獨加載,但你必須將註釋從服務器 HTML 中排除。不然 React 將不知道如何處理這塊 HTML(它的代碼在哪裏?),並在 hydration 過程當中刪除它。

在與任何東西互動以前,你必須 hydrate 全部的東西

hydration 自己也有一個相似的問題。現在,React 一次性完成樹的 hydration。這意味着,一旦它開始 hydrate(本質上是調用你的組件函數),React 就不會中止 hydration 的過程,直到它爲整個樹完成 hydration。所以,你必須等待全部的組件被 hydrated,才能與任何組件進行交互。

例如,咱們說評論小組件有昂貴的渲染邏輯。它在你的電腦上可能運行得很快,但在低端設備上運行這些邏輯的成本並不低,甚至可能使得屏幕被鎖定好幾秒鐘。固然,在理想狀況下,咱們在客戶端不會這樣的邏輯(這是服務器組件能夠幫助解決的問題)。但對於某些邏輯來講,這是不可避免的。這是由於它決定了所附的事件處理程序應該作什麼,並且對於交互性是相當重要的。所以,一旦開始 hydration,用戶就不能與導航欄、側邊欄或文章內容互動,直到整棵樹完成 hydration。對於導航來講,這是特別不幸的,由於用戶可能想徹底離開這個頁面,但因爲咱們正忙於 hydration,咱們把他們留在他們再也不關心的當前頁面上。

咱們如何解決這些問題?

這些問題之間有一個共同點。它們迫使你在早作一些事情(但由於它阻礙了全部其餘工做,致使用戶體驗被損害),或晚作一些事情(但由於你浪費時間,致使用戶體驗被損害)之間作出選擇。

這是由於有一個 「瀑布」(流程):獲取數據(服務器)→ 渲染成 HTML(服務器)→ 加載代碼(客戶端)→ hydration(客戶端)。任何一個階段都不能在前一個階段結束以前開始。 這就是爲何它的效率很低。咱們的解決方案是將工做分開,這樣咱們就能夠爲屏幕的一部分而不是整個應用程序作這些階段的工做。

這並非一個新奇的想法:好比說:Marko 是實現該模式的一個 JavaScript 網絡框架。將這樣的模式適應於 React 編程模型具備必定的挑戰性。咱們也所以花了一段時間來解決這個難題。咱們在 2018 年爲此目的引入了 <Suspense> 組件。當咱們引入它時,咱們只支持它在客戶端進行惰性加載代碼。但咱們的目標是將它與服務器渲染結合起來,解決這些問題。

讓咱們看看如何在 React 18 中使用 <Suspense> 來解決這些問題。

React 18:流式 HTML 和選擇性 hydration

在 React 18 中,有兩個主要的 SSR 功能是由 Suspense 解鎖的。

  • 在服務器上流式傳輸 HTML。要使用這個功能,你須要從 renderToString 切換到新的 pipeToNodeWritable 方法,如此處描述
  • 在客戶端進行選擇性的 hydration。要使用這個功能,你須要在客戶端 切換到createRoot,而後開始用 <Suspense> 包裝你的應用程序的一部分。

爲了瞭解這些功能的做用以及它們如何解決上述問題,讓咱們回到咱們的例子。

在全部數據被獲取以前,使用流式 HTML

現在的 SSR 中,渲染 HTML 和 hydration 是 「全有或全無」 的。首先,你要渲染全部的 HTML:

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section>
    <!-- Comments -->
    <p>First comment</p>
    <p>Second comment</p>
  </section>
</main>
複製代碼

客戶端最終會收到它:

而後你加載全部的代碼,並對整個應用程序進行 hydration:

可是 React 18 給了你一個新的可能性。你能夠用 <Suspense> 來包裝頁面的一部分。

例如,讓咱們包裹評論塊並告訴 React,在它準備好以前,React 應該顯示 <Spinner /> 組件。

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>
複製代碼

經過將 <Comments> 包裝成 <Suspense>,咱們告訴 React,它不須要等待評論就能夠開始爲頁面的其餘部分傳輸 HTML。相反,React 將發送佔位符(一個旋轉器)而不是評論:

如今在最初的 HTML 中找不到評論了:

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section id="comments-spinner">
    <!-- Spinner -->
    <img width=400 src="spinner.gif" alt="Loading..." />
  </section>
</main>
複製代碼

事情到這裏尚未結束。當服務器上的評論數據準備好後,React 會將額外的 HTML 發送到同一個流中,以及一個最小的內聯 <script> 標籤,將 HTML 放在 「正確的地方」。

<div hidden id="comments">
  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>
複製代碼

所以,甚至在 React 自己加載到客戶端以前,遲來的評論的 HTML 就會 「彈出」。

這就解決了咱們的第一個問題。如今你沒必要在顯示任何東西以前獲取全部的數據了。若是屏幕的某些部分延遲了最初的 HTML,你就沒必要在延遲全部的 HTML 或將其排除在 HTML 以外之間作出選擇。你能夠只容許那部份內容在 HTML 流中稍後 「涌入」。

不一樣於傳統的流式 HTML,它不必定要按照自上而下的順序發生。例如,若是側邊欄須要一些數據,你能夠用 Suspense 包裝它,React 將會發出一個佔位符,而後繼續渲染帖子。而後,當側邊欄的 HTML 準備好了,React 會把它和 <script> 標籤一塊兒流出來,把它插入到正確的位置 ——— 儘管帖子的 HTML(在樹中更遠的地方)已經被髮送出去了!沒有要求數據以任何特定的順序加載。你指定旋轉器應該出如今哪裏,剩下的就由 React 來解決。

注意事項:爲了使其發揮做用,你的數據獲取解決方案須要與 Suspense 集成。服務器組件將與 Suspense 開箱即用,但咱們也將爲獨立的 React 數據獲取庫提供一種方法來與之集成。

在全部代碼加載以前對頁面進行 hydration

咱們能夠提早發送最初的 HTML,但咱們仍然有一個問題。在加載評論小組件的 JavaScript 代碼以前,咱們不能在客戶端開始對咱們的應用程序進行 hydration。若是代碼的大小很大,這可能須要一段時間。

爲了不大型包,你一般會使用 「代碼拆分」:你能夠指定一段代碼不須要同步加載,你的打包工具將把它分割成一個單獨的 <script> 標籤。

你可使用 React.lazy 進行代碼分割,將評論代碼從主包中分割出來。

import { lazy } from 'react';

const Comments = lazy(() => import('./Comments.js'));

// ...

<Suspense fallback={<Spinner />}>
  <Comments />
</Suspense>
複製代碼

之前,這與服務器渲染中是不奏效的。(據咱們所知,即便是流行的變通方法也迫使你在選擇不使用代碼拆分組件的 SSR 或在全部代碼加載後對其進行 hydration 之間作出選擇,這在某種程度上違背了代碼拆分的目的)。

但在 React 18 中,<Suspense> 可讓你在評論小組件加載以前就 hydrate 應用程序。

從用戶的角度來看,最初他們看到的是以 HTML 形式流進來的非交互式內容。

而後你告訴 React 進行 hydration。雖然評論的代碼尚未出現,但也不要緊:

這是一個選擇性 hydration 的例子。經過將 Comments 包裹在 <Suspense>中,你告訴 React,他們不該該阻止頁面的其餘部分進行流式傳輸 ——— 並且,事實證實,也不該該阻止 hydration。這意味着第二個問題已經解決了:你再也不須要等待全部的代碼加載完成,才能開始 hydration。React 能夠在加載部分時同時進行 hydration。

React 會在評論部分的代碼加載完畢後開始對其部分進行 hydration:

得益於選擇性 hydration,一塊沉重的 JS 並不妨礙頁面的其餘部分具備交互性。

在全部的 HTML 都被流化以前,對頁面進行 hydration

React 會自動處理這一切,因此你不須要擔憂事情會以意外的順序發生。例如,也許 HTML 須要一段時間來加載,即便它正在被流化:

若是 JavaScript 代碼的加載時間早於全部的 HTML,React 就沒有理由等待了!它將爲頁面的其餘部分進行 hydration:

當評論的 HTML 加載時,由於 JS 尚未出現,因此它將顯示爲非交互式:

最後,當評論小組件的 JavaScript 代碼加載時,頁面將變得徹底可交互:

在全部組件完成 hydration 以前與頁面互動

當咱們將評論包裹在 <Suspense> 中時,還有一項改進發生在幕後。如今它們的 hydration 再也不阻礙瀏覽器作其餘工做。

例如,假設用戶在評論正在 hydration 時點擊了側邊欄:

在 React 18 中,瀏覽器能夠在給 Suspense 裏的內容進行 hydration 的過程當中出現的微小空隙中進行事件處理。得益於此,點擊被當即處理,在低端設備上長時間的 hydration 過程當中,瀏覽器不會出現卡頓。例如,這可讓用戶從他們再也不感興趣的頁面上導航離開。

在咱們的例子中,只有評論被包裹在 Suspense 中,因此對頁面的其餘部分進行 hydration 是一次性的。然而,咱們能夠經過在更多的地方使用 Suspense 來解決這個問題。例如,讓咱們把側邊欄也包起來。

<Layout>
  <NavBar />
  <Suspense fallback={<Spinner />}>
    <Sidebar />
  </Suspense>
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>
複製代碼

如今二者均可以在包含導航條和帖子的初始 HTML 以後從服務器上流傳。但這也會對 hydration 產生影響。比方說,它們的 HTML 已經加載,但它們的代碼尚未加載:

而後,包含側邊欄和評論代碼的包被加載。React 將嘗試對它們進行 hydration,從它在樹中較早發現的 Suspense 邊界開始(在這個例子中,它是側邊欄):

可是,假設用戶開始與評論小組件進行互動,其代碼也被加載:

React 會記錄點擊,並優先給評論進行 hydration,由於它更緊急:

在評論被 hydrated 後,React 「重放」 記錄的點擊事件(經過再次派發),並讓你的組件對互動作出反應。 而後,如今 React 沒有什麼緊急的事情要作,所以 React 會給側邊欄進行 hydration:

這解決了咱們的第三個問題。得益於選擇性 hydration,咱們沒必要 「爲了與任何東西互動而對全部東西進行 hydration」。 React 儘早開始給全部東西進行 hydration。它根據用戶的互動狀況,優先考慮屏幕上最緊急的部分。若是你考慮到在整個應用程序中採用 Suspense,邊界將變得更加細化,那麼選擇性 hydration 的好處就更加明顯:

在這個例子中,用戶點擊第一條評論時,正好是 hydration 的開始。React 會優先給全部父級 Suspense 邊界的內容進行 hydration,但會跳過任何不相關的兄弟節點。由於交互路徑上的組件優先被 hydrated,這創造了 hydration 是即時的的錯覺。React 會在以後對應用程序的其餘部分進行 hydration。

在實踐中,你可能會在你的應用程序的根部附近添加 Suspense:

<Layout>
  <NavBar />
  <Suspense fallback={<BigSpinner />}>
    <Suspense fallback={<SidebarGlimmer />}>
      <Sidebar />
    </Suspense>
    <RightPane>
      <Post />
      <Suspense fallback={<CommentsGlimmer />}>
        <Comments />
      </Suspense>
    </RightPane>
  </Suspense>
</Layout>
複製代碼

在這個例子中,最初的 HTML 能夠包括 <NavBar> 的內容,但其他的內容會在相關代碼加載後當即流入,並分部分進行 hydration,優先考慮用戶互動過的部分。

注意:你可能想知道你的應用程序如何能在這種不徹底 hydrated 的狀態下運做。設計中有一些微妙的細節,使其發揮做用。例如,不是對每一個單獨的組件分別進行 hydration,而是對整個 <Suspense> 邊界進行 hydration。由於 <Suspense> 已經被用於不會當即出現的內容,因此你的代碼對它的孩子不能當即出現的狀況有自適應性。React 老是以父級優先的順序進行 hydration,因此組件老是有它們的 props 組合。 React 在事件發生地的整個父樹 hydration 以前,暫不分派事件。最後,若是父類的更新方式致使還沒有 hydrated 的 HTML 變得陳舊,React 將隱藏它,並用你指定的 fallback 來代替它,直到代碼加載完畢。這確保了樹在用戶面前顯得一致。你不須要考慮這個,但這就是該功能發揮做用的緣由。

Demo

咱們準備了一個 你能夠嘗試的演示,看看新的 Suspense SSR 架構如何運做。它被人爲地放慢了速度,因此你能夠在 server/delays.js 中調整延時。

  • API_DELAY  讓你使評論在服務器上須要更長的時間來獲取,展現 HTML 的其餘部分如何提早發送。
  • JS_BUNDLE_DELAY  讓你延遲 <script> 標籤的加載,展現評論小組件的 HTML 如何在 React 和你的應用程序包下載以前 「彈出」。
  • ABORT_DELAY 讓你看到服務器 「放棄」,並在服務器上獲取時間過長時將渲染工做移交給客戶端。

總結

React 18 爲 SSR 提供了兩個主要功能:

  • 流式 HTML 讓你儘早開始發送 HTML,流式 HTML 的額外內容與 <script> 標籤一塊兒放在正確的地方。
  • 選擇性 hydration 讓你在 HTML 和 JavaScript 代碼徹底下載以前,儘早開始爲你的應用程序進行 hydration。它還優先爲用戶正在互動的部分進行 hydration,創造一種即時 hydration 的錯覺。

這些功能解決了 React 中 SSR 的三個長期存在的問題:

  • 你再也不須要等待全部的數據在服務器上加載後再發送 HTML。相反,一旦你有足夠的數據來顯示應用程序的外殼,你就開始發送 HTML,其他的 HTML 在準備好後再進行流式傳輸。
  • 你再也不須要等待全部的 JavaScript 加載來開始 hydration。相反,你可使用代碼拆分和服務器渲染。服務器 HTML 將被保留,React 將在相關代碼加載時對其進行 hydration。
  • 你再也不須要等待全部的組件被 hydrated 後纔開始與頁面互動了。相反,你能夠依靠選擇性 hydration,來優先考慮用戶正在與之互動的組件,並儘早對它們進行 hydration。

Suspense 組件做爲全部這些功能的選擇。這些改進自己是在 React 內部自動進行的,咱們指望它們能與大多數現有的 React 代碼一塊兒使用。這展現了聲明性地表達加載狀態的力量。從 if (isLoading)<Suspense> 可能看不出很大的變化,但它倒是解鎖這些改進的關鍵。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索