如何在 React 18中 利用Suspense 實(shí)現(xiàn) 服務(wù)端渲染(SSR)

如何在服務(wù)端使用React18
https://github.com/reactwg/react-18/discussions/22
服務(wù)器端渲染(在本文中縮寫為“SSR”)讓您可以從服務(wù)器上的 React 組件生成 HTML,并將該 HTML 發(fā)送給您的用戶。SSR 允許您的用戶在您的 JavaScript 包加載和運(yùn)行之前查看頁面的內(nèi)容。
React 中的 SSR 總是發(fā)生在幾個(gè)步驟中:
在服務(wù)器端,獲取整個(gè)應(yīng)用程序的數(shù)據(jù)。
在服務(wù)器端,將整個(gè)應(yīng)用程序呈現(xiàn)為 HTML 字符串并將其發(fā)送到客戶端。
在客戶端,加載整個(gè)應(yīng)用程序的JavaScript 代碼。
在客戶端,將JavaScript 邏輯連接到整個(gè)應(yīng)用程序的服務(wù)器生成的HTML(這就是“hydration”)。
關(guān)鍵部分是,在下一步開始之前,整個(gè)應(yīng)用程序的每個(gè)步驟都必須立即完成。如果其中一個(gè)環(huán)節(jié)比其他部分慢,將會(huì)影響整體的加載時(shí)間。
React18中,您可以使用 <Suspense> 將您的應(yīng)用程序分解成更小的獨(dú)立單元,每個(gè)模塊都是獨(dú)自異步加載,并不會(huì)影響其余部分。即便是應(yīng)用程序中最慢的模塊也不會(huì)拖累較快的模塊。因此,用戶也將更快地看到內(nèi)容,并更快的開始與之交互。

此插圖使用綠色表示頁面的這些部分是交互式的。換句話說,它們所有的 JavaScript 事件處理程序都已附加,單擊按鈕可以更新狀態(tài),等等。
但是,頁面在 JavaScript 代碼完全加載之前無法進(jìn)行交互。這包括 React 本身和您的應(yīng)用程序代碼。對(duì)于非React的應(yīng)用程序,大部分加載時(shí)間將用于下載您的應(yīng)用程序代碼。
如果您不使用 SSR,則用戶在 JavaScript 加載時(shí)只會(huì)看到一個(gè)空白頁面:


當(dāng) React 和你的應(yīng)用程序代碼都加載時(shí),你想讓這個(gè) HTML 交互。你告訴 React:“這是App在服務(wù)器上生成這個(gè) HTML的組件。將事件處理程序附加到該 HTML!” React 將在內(nèi)存中渲染你的組件樹,但它不會(huì)為它生成 DOM 節(jié)點(diǎn),而是將所有邏輯附加到現(xiàn)有的 HTML。
渲染組件和附加事件處理程序的過程稱為“水化”。這就像用交互性和事件處理程序的“水”去澆灌“干涸”的 HTML。(或者至少,這就是我對(duì)自己解釋這個(gè)術(shù)語的方式。)
水合之后,它是“像往常一樣反應(yīng)”:你的組件可以設(shè)置狀態(tài),響應(yīng)點(diǎn)擊等等:

你可以看到 SSR 是一種“魔術(shù)”。它不會(huì)使您的應(yīng)用程序完全交互更快。相反,它可以讓您更快地顯示應(yīng)用程序的非交互式版本,以便用戶可以在等待 JS 加載時(shí)查看靜態(tài)內(nèi)容。然而,這個(gè)技巧對(duì)網(wǎng)絡(luò)連接不佳的人產(chǎn)生了巨大的影響,并提高了整體感知性能。由于其更容易的索引和更快的速度,它還可以幫助您進(jìn)行搜索引擎排名。
注意:不要將 SSR 與服務(wù)器組件混淆。服務(wù)器組件是一個(gè)更具實(shí)驗(yàn)性的功能,仍在研究中,可能不會(huì)成為最初的 React 18 版本的一部分。您可以復(fù)制以下連接了解服務(wù)器組件。服務(wù)器組件是對(duì) SSR 的補(bǔ)充,并將成為推薦的數(shù)據(jù)獲取方法的一部分,但本文與它們無關(guān)。
React服務(wù)器組件
https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html
上述方法有效,但在很多方面表現(xiàn)不佳。
1. 您必須獲取所有內(nèi)容,然后才能顯示任何內(nèi)容【整個(gè)過程是 同步進(jìn)行的】
今天 SSR 的一個(gè)問題是它不允許組件“等待數(shù)據(jù)”。使用當(dāng)前的 API,當(dāng)您呈現(xiàn)為 HTML 時(shí),您必須為服務(wù)器上的組件準(zhǔn)備好所有數(shù)據(jù)。這意味著您必須先收集服務(wù)器上的所有數(shù)據(jù),然后才能開始向客戶端發(fā)送任何HTML。這是相當(dāng)?shù)托У摹?/span>
例如,假設(shè)您要呈現(xiàn)帶有評(píng)論的帖子。盡早顯示注釋很重要,因此您希望將它們包含在服務(wù)器 HTML 輸出中。但是您的數(shù)據(jù)庫或 API 層很慢,這是您無法控制的?,F(xiàn)在你必須做出一些艱難的選擇。如果您將它們從服務(wù)器輸出中排除,則在 JS 加載之前用戶將不會(huì)看到它們。但是,如果您將它們包含在服務(wù)器輸出中,則必須延遲發(fā)送其余的 HTML(例如,導(dǎo)航欄、側(cè)邊欄,甚至是帖子內(nèi)容),直到評(píng)論加載完畢并且您可以呈現(xiàn)完整的樹。這不是很友好。
作為旁注,一些數(shù)據(jù)獲取解決方案反復(fù)嘗試將樹渲染為 HTML 并丟棄結(jié)果,直到數(shù)據(jù)得到解析,因?yàn)?React 沒有提供更符合人體工程學(xué)的選項(xiàng)。我們希望提供一種不需要如此極端妥協(xié)的解決方案。
2. 給任何板塊補(bǔ)水之前,您必須加載所有數(shù)據(jù)
在您的 JavaScript 代碼加載后,您將告訴 React “水合” HTML 并使其具有交互性。React 將在渲染組件時(shí)“遍歷”服務(wù)器生成的 HTML,并將事件處理程序附加到該 HTML。為此,瀏覽器中組件生成的樹必須與服務(wù)器生成的樹相匹配。否則 React 無法“匹配它們!” 這樣做的一個(gè)非常不幸的后果是,您必須先為客戶端上的所有組件加載 JavaScript,然后才能開始對(duì)它們中的任何一個(gè)進(jìn)行補(bǔ)水。
例如,假設(shè)評(píng)論小部件包含很多復(fù)雜的交互邏輯,為其加載 JavaScript 需要一段時(shí)間?,F(xiàn)在你必須再次做出艱難的選擇。最好將服務(wù)器上的評(píng)論呈現(xiàn)為 HTML,以便盡早將它們顯示給用戶。但是因?yàn)榻裉熘荒芤淮瓮瓿裳a(bǔ)水,所以在您加載評(píng)論小部件的代碼之前,您無法開始對(duì)導(dǎo)航欄、側(cè)邊欄和帖子內(nèi)容進(jìn)行補(bǔ)水!當(dāng)然,您可以使用代碼拆分并單獨(dú)加載它,但是您必須從服務(wù)器 HTML 中排除注釋。否則 React 將不知道如何處理這塊 HTML(它的代碼在哪里?)并在水化過程中將其刪除。
3. 在與任何事物交互之前,您必須先補(bǔ)充所有水分
融合作用本身也存在類似的問題。今天,React 一次性完成樹的水化。這意味著一旦它開始 hydrating(本質(zhì)上是調(diào)用你的組件函數(shù)),React 不會(huì)停止,直到它為整個(gè)樹完成此操作。因此,您必須等待所有組件都 “融合” 后才能與它們中的任何一個(gè)進(jìn)行交互。
例如,假設(shè)評(píng)論小部件具有昂貴的渲染邏輯。它可能在您的計(jì)算機(jī)上運(yùn)行得很快,但在運(yùn)行所有這些邏輯的低端設(shè)備上并不便宜,甚至可能會(huì)鎖定屏幕幾秒鐘。當(dāng)然,理想情況下,我們根本不會(huì)在客戶端上有這樣的邏輯(服務(wù)器組件可以提供幫助)。但是對(duì)于某些邏輯來說,這是不可避免的,因?yàn)樗鼪Q定了附加的事件處理程序應(yīng)該做什么并且對(duì)于交互性至關(guān)重要。因此,一旦水化開始,用戶就無法與導(dǎo)航欄、側(cè)邊欄或帖子內(nèi)容進(jìn)行交互,直到整個(gè)樹被水化。對(duì)于導(dǎo)航,這尤其令人遺憾,因?yàn)橛脩艨赡芟M耆x開此頁面——但由于我們正忙于補(bǔ)充水分,我們將它們保留在他們不再關(guān)心的當(dāng)前頁面上。
這些問題之間有一個(gè)共同點(diǎn)。它們迫使你在早點(diǎn)做某事(但因?yàn)樗柚顾衅渌ぷ鞫鴵p害用戶體驗(yàn))或晚做某事(但因?yàn)槟憷速M(fèi)時(shí)間而損害用戶體驗(yàn))之間做出選擇。
這是因?yàn)橛幸粋€(gè)過程:獲取數(shù)據(jù)(服務(wù)器)→ 渲染到 HTML(服務(wù)器)→ 加載代碼(客戶端)→ 水合物(客戶端)。在應(yīng)用程序的前一階段完成之前,這兩個(gè)階段都不能開始。這就是它效率低下的原因。我們的解決方案是將工作分開,以便我們可以為屏幕的一部分而不是整個(gè)應(yīng)用程序執(zhí)行每個(gè)階段。
這不是一個(gè)新穎的想法:例如,
Marko[https://tech.ebayinc.com/engineering/async-fragments-rediscovering-progressive-html-rendering-with-marko/] 是實(shí)現(xiàn)此模式版本的 JavaScript Web 框架之一。挑戰(zhàn)在于如何使這樣的模式適應(yīng) React 編程模型?;艘欢螘r(shí)間才解決。我們<Suspense>在 2018 年為此目的引入了該組件。我們引入它時(shí)僅支持在客戶端延遲加載代碼。但目標(biāo)是將其與服務(wù)器渲染集成并解決這些問題。
讓我們看看如何<Suspense>在 React 18 中使用來解決這些問題。
Suspense 解鎖的 React 18 中有兩個(gè)主要的 SSR 特性:
在服務(wù)器上流式傳輸 HTML。要選擇使用它,您需要renderToString從新pipeToNodeWritable方法切換到新方法,如此處所述。
對(duì)客戶進(jìn)行選擇性水合作用。要選擇加入它,您需要在客戶端上切換到createRoot,然后開始使用<Suspense>。(https://github.com/reactwg/react-18/discussions/5)
要了解這些功能的作用以及它們?nèi)绾谓鉀Q上述問題,讓我們返回到我們的示例。
<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>


例如,讓我們包裝注釋塊并告訴 React,在它準(zhǔn)備好之前,React 應(yīng)該顯示該<Spinner />組件:
<Layout><NavBar /><Sidebar /><RightPane><Post /><Suspense fallback={<Spinner />}><Comments /></Suspense></RightPane></Layout>
將<Comments>包裝成<Suspense>,我們告訴 React它不需要等待評(píng)論開始為頁面的其余部分流式傳輸 HTML。相反,React 將發(fā)送占位符(一個(gè)微調(diào)器)而不是評(píng)論:

<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>
故事到這里還沒有結(jié)束。當(dāng)評(píng)論的數(shù)據(jù)在服務(wù)器上準(zhǔn)備好時(shí),React會(huì)將額外的 HTML 發(fā)送到同一個(gè)流中,以及一個(gè)最小的內(nèi)聯(lián)<script>標(biāo)簽,以將該 HTML 放在“正確的位置”:
<div hidden id="comments"><!-- Comments --><p>First comment</p><p>Second comment</p></div><script>// This implementation is slightly simplifieddocument.getElementById('sections-spinner').replaceChildren(document.getElementById('comments'));</script>
結(jié)果,即使在 React 本身加載到客戶端之前,遲來的 HTML 評(píng)論也會(huì)“彈出”:

這就解決了我們的第一個(gè)問題?,F(xiàn)在,您不必先獲取所有數(shù)據(jù),然后才能顯示任何內(nèi)容。如果屏幕的某些部分延遲了初始 HTML,則您不必在延遲所有HTML 或?qū)⑵鋸?HTML 中排除之間做出選擇。您可以只允許該部分稍后在 HTML 流中“彈出”。
與傳統(tǒng)的 HTML 流不同,它不必按自上而下的順序發(fā)生。例如,如果側(cè)邊欄需要一些數(shù)據(jù),您可以將其包裝在 Suspense 中,React 會(huì)發(fā)出一個(gè)占位符并繼續(xù)渲染帖子。然后,當(dāng)側(cè)邊欄 HTML 準(zhǔn)備好時(shí),React 會(huì)將其與將<script>其插入正確位置的標(biāo)簽一起流式傳輸——即使帖子的 HTML(在樹中更遠(yuǎn)的位置)已經(jīng)發(fā)送!不要求以任何特定順序加載數(shù)據(jù)。您指定微調(diào)器應(yīng)該出現(xiàn)的位置,React 會(huì)找出其余的。
注意:為此,您的數(shù)據(jù)獲取解決方案需要與 Suspense 集成。服務(wù)器組件將開箱即用地與 Suspense 集成,但我們還將提供一種方法讓獨(dú)立的 React 數(shù)據(jù)獲取庫與之集成。
在所有代碼加載之前對(duì)頁面進(jìn)行“水分”補(bǔ)充
我們可以更早地發(fā)送初始 HTML,但我們?nèi)匀挥袉栴}。在評(píng)論小部件的 JavaScript 代碼加載之前,我們無法開始在客戶端上對(duì)我們的應(yīng)用程序進(jìn)行補(bǔ)水。如果代碼很大,這可能需要一段時(shí)間。
為了避免較大體積的組件包,你通常會(huì)使用“代碼拆分”:你會(huì)指定一段代碼不需要同步加載,你的捆綁器會(huì)將它拆分成一個(gè)單獨(dú)的<script>標(biāo)簽。
您可以使用代碼拆分React.lazy從主包中拆分注釋代碼:
import { lazy } from 'react';const Comments = lazy(() => import('./Comments.js'));// ...<Suspense fallback={<Spinner />}><Comments /></Suspense>
以前,這不適用于服務(wù)器渲染。(據(jù)我們所知,即使是流行的解決方法也迫使您在選擇退出代碼拆分組件的 SSR 或在所有代碼加載后對(duì)其進(jìn)行補(bǔ)充之間做出選擇,這在某種程度上違背了代碼拆分的目的。)
但是在 React 18 中,<Suspense>可以讓您在評(píng)論小部件加載之前對(duì)應(yīng)用程序 進(jìn)行補(bǔ)水。
從用戶的角度來看,最初他們會(huì)看到以 HTML 形式流入的非交互式內(nèi)容:


然后您告訴React去“水合”,評(píng)論的代碼還沒有,但沒有關(guān)系:

這是選擇性水合作用的一個(gè)例子。通過包裝Comments中<Suspense>,你告訴陣營,他們不應(yīng)該阻止頁面的其余部分流和,事實(shí)證明,水化,太!這意味著第二個(gè)問題解決了:您不再需要等待所有代碼加載才能開始補(bǔ)水。React 可以在加載部件時(shí)對(duì)其進(jìn)行水合。
React 將在其代碼加載完成后開始為評(píng)論部分補(bǔ)水:

多虧了 Selective Hydration,大量的 JS 不會(huì)阻止頁面的其余部分變得可交互。
在流式傳輸所有代碼之前對(duì)頁面進(jìn)行“水分”補(bǔ)充
React 會(huì)自動(dòng)處理所有這些,因此您無需擔(dān)心事情會(huì)以意外的順序發(fā)生。例如,即使 HTML 正在流式傳輸,它也可能需要一段時(shí)間才能加載:

如果 JavaScript 代碼早于所有 HTML 加載,React 沒有理由等待!它將滋潤頁面的其余部分:

當(dāng)注釋的 HTML 加載時(shí),它將顯示為非交互式,因?yàn)?JS 還沒有:


當(dāng)我們將評(píng)論包裹在<Suspense>. 現(xiàn)在它們的水分不再阻止瀏覽器做其他工作。
例如,假設(shè)用戶在添加評(píng)論時(shí)單擊側(cè)邊欄:

在 React 18 中,Suspense 邊界內(nèi)的水化內(nèi)容發(fā)生在瀏覽器可以處理事件的微小間隙中。多虧了這一點(diǎn),點(diǎn)擊會(huì)立即處理,并且在低端設(shè)備上長時(shí)間水合期間,瀏覽器不會(huì)出現(xiàn)卡住現(xiàn)象。例如,這讓用戶可以離開他們不再感興趣的頁面。
在我們的例子中,只有評(píng)論被包裹在 Suspense 中,所以頁面的其余部分在一次傳遞中發(fā)生。但是,我們可以通過在更多地方使用 Suspense 來解決這個(gè)問題!例如,讓我們也包裝側(cè)邊欄:
<Layout><NavBar /><Suspense fallback={<Spinner />}><Sidebar /></Suspense><RightPane><Post /><Suspense fallback={<Spinner />}><Comments /></Suspense></RightPane></Layout>
現(xiàn)在兩個(gè)人可以從服務(wù)器包含導(dǎo)航欄和后最初的HTML之后流。但這也會(huì)影響水合作用。假設(shè)它們兩個(gè)的 HTML 都已加載,但它們的代碼尚未加載:

然后,包含側(cè)邊欄和注釋代碼的包加載。React 將嘗試將它們都水化,從它在樹中較早找到的 Suspense 邊界開始(在本例中,它是側(cè)邊欄):

但是假設(shè)用戶開始與評(píng)論小部件交互,為此還加載了代碼:

React 會(huì)記錄發(fā)生的點(diǎn)擊,并優(yōu)先處理評(píng)論,因?yàn)樗o急:

在評(píng)論“水合”之后,React“重放”記錄的點(diǎn)擊事件(通過再次調(diào)度它)并讓您的組件響應(yīng)交互。然后,既然 React 無事可做,React 將“水化” 側(cè)邊欄:

這就解決了我們的第三個(gè)問題。多虧了選擇性水合作用,我們不必“為了與任何東西互動(dòng)而將所有東西都水化”。React 會(huì)盡早開始為所有內(nèi)容補(bǔ)水,并根據(jù)用戶交互優(yōu)先考慮屏幕上最緊急的部分。如果您考慮到在整個(gè)應(yīng)用程序中采用 Suspense 時(shí),邊界將變得更加細(xì)化,則選擇性水化的好處將變得更加明顯:

在此示例中,用戶在水合開始時(shí)單擊第一條評(píng)論。React 將優(yōu)先處理所有父 Suspense 邊界的內(nèi)容,但會(huì)跳過任何不相關(guān)的兄弟姐妹。這會(huì)產(chǎn)生一種錯(cuò)覺,即水合是即時(shí)的,因?yàn)榻换ヂ窂缴系慕M件首先被水合。React 將立即為應(yīng)用程序的其余部分補(bǔ)水。
在實(shí)踐中,您可能會(huì)在應(yīng)用程序的根目錄附近添加 Suspense:
<Layout><NavBar /><Suspense fallback={<BigSpinner />}><Suspense fallback={<SidebarGlimmer />}><Sidebar /></Suspense><RightPane><Post /><Suspense fallback={<CommentsGlimmer />}><Comments /></Suspense></RightPane></Suspense></Layout>
在此示例中,初始 HTML 可以包含<NavBar>內(nèi)容,但其余部分將在加載相關(guān)代碼后立即流入并混合部分,優(yōu)先考慮用戶與之交互的部分。
注意:您可能想知道您的應(yīng)用程序如何在這種非完全水合狀態(tài)下工作。設(shè)計(jì)中有一些微妙的細(xì)節(jié)使其發(fā)揮作用。例如,不是單獨(dú)對(duì)每個(gè)單獨(dú)的組件進(jìn)行水合,而是對(duì)整個(gè)<Suspense>邊界進(jìn)行水合。由于<Suspense>已用于不會(huì)立即出現(xiàn)的內(nèi)容,因此您的代碼對(duì)其子項(xiàng)不立即可用具有彈性。React 總是按照父級(jí)優(yōu)先順序進(jìn)行 hydration,因此組件總是設(shè)置了它們的 props。React 推遲調(diào)度事件,直到從事件點(diǎn)開始的整個(gè)父節(jié)點(diǎn)都被水合。最后,如果父級(jí)更新導(dǎo)致尚未水合的 HTML 變得陳舊,React 將隱藏它并將其替換為fallback您指定直到代碼加載完畢。這確保了樹對(duì)用戶來說是一致的。你不需要考慮它,但這就是讓它起作用的原因。
我們準(zhǔn)備了一個(gè)演示,您可以嘗試了解新的 Suspense SSR 架構(gòu)如何工作。它被人為地減慢,因此您可以調(diào)整延遲server/delays.js:
API_DELAY 允許您在服務(wù)器上獲取更長的注釋,展示如何盡早發(fā)送 HTML 的其余部分。
JS_BUNDLE_DELAY讓你延遲<script>標(biāo)簽加載,展示評(píng)論小部件的 HTML 如何在 React 和你的應(yīng)用程序包下載之前“彈出”。
ABORT_DELAY 如果在服務(wù)器上獲取時(shí)間太長,您可以看到服務(wù)器“放棄”并將渲染交給客戶端。
https://codesandbox.io/s/github/facebook/react/tree/master/fixtures/ssr2?file=/src/App.jsReact 18 為 SSR 提供了兩個(gè)主要特性:
流式 HTML允許您盡早開始輸出 HTML,將附加內(nèi)容的 HTML 與<script>將它們放在正確位置的標(biāo)簽一起流式傳輸。
Selective Hydration可讓您在剩余的 HTML 和 JavaScript 代碼完全下載之前盡早開始對(duì)應(yīng)用程序進(jìn)行補(bǔ)充。它還優(yōu)先考慮為用戶與之交互的部分補(bǔ)水,從而產(chǎn)生即時(shí)補(bǔ)水的錯(cuò)覺。
這些特性解決了 React 中 SSR 的三個(gè)長期存在的問題:
在發(fā)送 HTML 之前,您不再需要等待所有數(shù)據(jù)加載到服務(wù)器上。相反,當(dāng)您有足夠的內(nèi)容顯示應(yīng)用程序的外殼時(shí),您立即開始發(fā)送 HTML,并在準(zhǔn)備好時(shí)流式傳輸其余的 HTML。
您不再需要等待所有 JavaScript 加載完畢才能開始補(bǔ)水。相反,您可以將代碼拆分與服務(wù)器渲染一起使用。服務(wù)器 HTML 將被保留,當(dāng)相關(guān)代碼加載時(shí),React 將對(duì)其進(jìn)行水合。
您不再需要等待所有組件都開始與頁面交互。相反,您可以依靠 Selective Hydration 來確定用戶與之交互的組件的優(yōu)先級(jí),并盡早對(duì)它們進(jìn)行交互。
該<Suspense>組件可作為所有這些功能的選擇。改進(jìn)本身在 React 內(nèi)部是自動(dòng)的,我們希望它們能夠與大多數(shù)現(xiàn)有的 React 代碼一起使用。這展示了以聲明方式表達(dá)加載狀態(tài)的能力。從if (isLoading)到看起來可能沒有很大的變化<Suspense>,但它是解鎖所有這些改進(jìn)的原因。
以上譯文,避免不了措辭不當(dāng)之處,還請(qǐng)諒解,如需查看原文請(qǐng)?jiān)L問如下鏈接:
https://github.com/reactwg/react-18/discussions/37
?? 謝謝支持
以上便是本次分享的全部內(nèi)容,希望對(duì)你有所幫助^_^
喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~。
歡迎關(guān)注公眾號(hào) 趣談前端 收貨大廠一手好文章~

從零搭建全棧可視化大屏制作平臺(tái)V6.Dooring
點(diǎn)個(gè)在看你最好看
