React Suspense 的發(fā)展歷程頗為曲折:多年間它幾乎沒有被使用,且被認(rèn)為收效甚微,僅是呈現(xiàn)加載狀態(tài)的一種炫酷方式保檐。然而,隨著 React 18 的推出崔梗,Suspense 提供了一整套新的優(yōu)勢夜只,值得重新審視。遺憾的是蒜魄,這些優(yōu)勢從平淡無奇到較為深奧不等扔亥,并且對應(yīng)用架構(gòu)的依賴性較高爪膊。接下來我們將看看當(dāng)今最常見的三種渲染架構(gòu),以及 React Suspense 在其中所扮演的角色砸王。
簡要概述
注意:
如果將來禁用此彈窗推盛,點擊標(biāo)題將會將鏈接復(fù)制到剪貼板。這是一個很棒的 URL谦铃,當(dāng)頁面加載時它將滾動到對應(yīng)的部分耘成!
關(guān)閉
- 客戶端渲染:在 React.lazy 加載時顯示回退狀態(tài);使用支持 suspense 的框架以聲明方式處理數(shù)據(jù)加載和錯誤狀態(tài)驹闰。
- 服務(wù)端渲染:上述功能 + 將服務(wù)端渲染的組件包裹在
<Suspense />
中瘪菌,以便在客戶端選擇性地進行水合。 - 服務(wù)端組件:上述功能 + 將異步服務(wù)端組件包裹在
<Suspense />
中嘹朗,分階段流式傳輸?shù)娇蛻舳耍菏紫仁腔赝藸顟B(tài)师妙,接著是最終內(nèi)容。
深入探討
客戶端渲染
這是最基本的 React 用法屹培∧ǎ客戶端請求時,服務(wù)器返回一個只包含基本 HTML 的文件褪秀,其中包含引用 JavaScript 包的 <script>
標(biāo)簽蓄诽。當(dāng) JavaScript 加載并執(zhí)行后,生成頁面內(nèi)容并填充空白的 HTML 文件媒吗。所有導(dǎo)航完全在客戶端進行仑氛,不再向服務(wù)器發(fā)出額外請求——這引出了 Suspense 的第一個用例。由于 JavaScript 包含了生成應(yīng)用任何部分所需的代碼闸英,因此文件體積可能較大锯岖。頁面內(nèi)容在渲染之前,必須加載甫何、解析并執(zhí)行整個 JavaScript 文件出吹,這成為一個嚴(yán)重的性能瓶頸。不過沛豌,你不需要在每個頁面加載所有代碼趋箩。我們可以將應(yīng)用拆分成多個不同的 JavaScript 包,僅在必要時將各自包發(fā)送到客戶端加派?這就是 Suspense 和 React.lazy 的用武之地。
使用 Suspense 和 React.lazy
React.lazy 的核心功能是傳入一個返回 Promise 的函數(shù)跳芳,以懶加載 React 組件芍锦,Promise 最終會解析為一個組件。大多數(shù)情況下飞盆,我們使用動態(tài)導(dǎo)入語法來懶加載模塊:
const Post = lazy(() => import('./Post.ts'));
結(jié)合 Suspense娄琉,我們可以在導(dǎo)入加載期間向 React 指示渲染一個回退加載狀態(tài):
export default function Wrapper() {
return (
<Suspense fallback={<div>加載中...</div>}>
<Post />
</Suspense>
);
}
如果使用了像 React Router 這樣的導(dǎo)航庫次乓,可以通過路由對應(yīng)用進行代碼拆分,在各頁面的 Route 組件中分別懶加載入口點孽水。
當(dāng)然票腰,可以自行實現(xiàn)這種行為——在動態(tài)導(dǎo)入組件時渲染加載狀態(tài)——但使用 Suspense 會更優(yōu)雅。這也引發(fā)了一個問題:能否用 Suspense 簡化所有使用 useEffect
完成的數(shù)據(jù)獲取操作女气?
在 useEffect 中使用 Suspense 進行數(shù)據(jù)獲取
在進一步探討之前杏慰,我們先來簡單了解一下 <Suspense />
的內(nèi)部機制。主要問題是:父級 <Suspense />
是如何知道子組件正在加載的炼鞠?據(jù)我所知缘滥,只有兩種方法可以讓子組件改變其父組件的狀態(tài):
- 子組件修改父組件使用的狀態(tài)
- 子組件拋出一個值,父組件可以捕獲并處理
第二種方式通常表現(xiàn)為一個錯誤邊界(Error Boundary)谒主,這是一個 React 組件朝扼,專門用于捕獲子組件在程序崩潰時拋出的非預(yù)期錯誤。有趣的是霎肯,React 還將這一機制應(yīng)用到了其他場景:Suspense 依賴子組件拋出一個 Promise擎颖。
簡單來說,子組件會在加載時拋出一個掛起狀態(tài)的 Promise观游,并在準(zhǔn)備好渲染時解析肠仪。父組件會捕獲該 Promise,并根據(jù)情況渲染 fallback
屬性或子組件內(nèi)容备典。
可以想象异旧,設(shè)置一個支持 suspense 的數(shù)據(jù)獲取工具是非常復(fù)雜的,使用一個現(xiàn)成的庫會更加省心提佣。
這就是說吮蛹,如果你的庫支持 suspense,你可以將組件包裹在 <Suspense />
中拌屏,指定一個回退狀態(tài)潮针,并添加一個錯誤邊界以捕獲被拒絕的 Promise,省去手動管理 isLoading
或 isError
狀態(tài)的麻煩倚喂!
function Post() {
// 例如每篷,使用 React Query
const { data } = useQuery({ suspense: true });
// 可以假設(shè)數(shù)據(jù)已完全獲取,因為在數(shù)據(jù)加載時 React
// 會“掛起”組件端圈,而這段代碼不會執(zhí)行焦读!
return <div>{data}</div>;
}
export default function Wrapper() {
return (
<ErrorBoundary fallbackRender={<div>出錯了!</div>}>
<Suspense fallback={<div>加載中...</div>}>
<Post />
</Suspense>
</ErrorBoundary>
);
}
對于一些人來說舱权,這看起來更簡潔矗晃;但對另一些人而言,這可能是“毀滅金字塔”(pyramid of doom)的早期跡象宴倍,他們更愿意自己處理加載和錯誤狀態(tài)张症。無論如何仓技,很難說 Suspense 的數(shù)據(jù)獲取開啟了無法手動實現(xiàn)的全新功能——那些功能還會陸續(xù)登場。
服務(wù)端渲染(Server-Side Rendering)
在服務(wù)端渲染應(yīng)用中俗他,Suspense 開啟了一些非常有趣的新功能——但首先脖捻,我們需要簡要了解一下水合(hydration)的基礎(chǔ)。
在請求時兆衅,元框架(如 Next.js)通過運行相關(guān)文件中導(dǎo)出的組件來生成頁面的 HTML地沮,用于首次渲染。這段 HTML 會發(fā)送給用戶涯保,以便在 JavaScript 包加載時看到有意義的內(nèi)容诉濒。當(dāng) JavaScript 到達(dá)時,元框架會在客戶端重新運行組件夕春,確保生成的 DOM 與服務(wù)器上生成的 HTML 相同(若不一致未荒,通常會有警告)。此時及志,除了生成狀態(tài)片排、綁定事件等 JavaScript 功能也已到位。重新在客戶端運行組件的整個過程稱為水合速侈。
相比客戶端渲染率寡,服務(wù)端渲染在首次加載頁面時提供了更好的用戶體驗,因為用戶可以看到服務(wù)器生成的 HTML倚搬,而 JavaScript 包正在加載和執(zhí)行中冶共。不過,僅僅是“看到”——沒有 JavaScript 頁面無法交互每界。問題在于捅僵,整個頁面在可以交互之前都需要被水合完成!這就是 Suspense 的第三個用例:選擇性水合眨层。
通過將組件包裹在 Suspense 中庙楚,React 會將其與頁面的其余部分分開水合。乍看之下趴樱,這似乎沒什么用:如果所有非水合的 HTML 都同時發(fā)送到客戶端馒闷,整個頁面還是會一起被水合。然而叁征,這不完全正確纳账,原因如下:
如果將多個組件包裹在 Suspense 中,React 可以根據(jù)用戶當(dāng)前的交互航揉,智能決定先水合哪個部分塞祈。也就是說,React 可以優(yōu)先水合頁面的某部分帅涂,從而讓用戶先使用到一個交互控件议薪,同時后臺繼續(xù)水合頁面的其余部分。對處理 JavaScript 較慢的設(shè)備而言媳友,這可以極大地提升用戶體驗斯议。
在流式傳輸架構(gòu)中,頁面的不同部分可以分開發(fā)送到客戶端醇锚,這意味著某塊 HTML 可以在其他部分還在服務(wù)器渲染時先發(fā)送到客戶端并選擇性地進行水合哼御!
需要注意的是,當(dāng)前的 SSR 框架并不支持選擇性水合——據(jù)我所知焊唬,只有使用 app
目錄的 Next.js 應(yīng)用支持將客戶端組件渲染成服務(wù)器上的 HTML 并進行選擇性水合恋昼。可以查看我關(guān)于 SSR 與服務(wù)器組件的博文了解更多信息赶促!
服務(wù)器組件(Server Components)
服務(wù)器組件簡單來說是指在服務(wù)器上渲染為 HTML 的 React 組件液肌,然后將其發(fā)送到客戶端。聽起來像是服務(wù)端渲染鸥滨,但服務(wù)器組件僅在服務(wù)器端運行嗦哆,永不在客戶端執(zhí)行。它們不能使用事件處理器婿滓、狀態(tài)或 Hooks老速,因此本質(zhì)上是非交互式的。服務(wù)器組件主要用于獲取和渲染靜態(tài)數(shù)據(jù):
export default async function Post() {
const data = await fetch(...)
return <div>{data}</div>
}
注意這里的函數(shù)/組件是異步的凸主!可以直接等待數(shù)據(jù)加載完成橘券,然后將內(nèi)容渲染為 HTML 并發(fā)送到客戶端。這個方式簡單優(yōu)雅卿吐,但用戶體驗欠佳——在異步操作完成前旁舰,組件會被阻塞,用戶無法看到任何內(nèi)容但两!這種情況正是加載狀態(tài)的用武之地鬓梅。那么,如何為服務(wù)器組件提供一個加載狀態(tài)谨湘?可以使用 Suspense绽快。
將異步服務(wù)器組件包裹在 <Suspense />
中,React 會在組件獲取數(shù)據(jù)時渲染并發(fā)送 fallback
到客戶端紧阔。一旦數(shù)據(jù)加載完成坊罢,React 就會發(fā)送組件的渲染內(nèi)容。這種分階段將多塊 HTML 發(fā)送到客戶端的過程稱為流式傳輸(streaming)擅耽。
async function Post() {
const data = await fetch(...)
return <div>{data}</div>
}
export default function Wrapper() {
return (
<Suspense fallback={<div>加載中...</div>}>
<Post />
</Suspense>
);
}
總結(jié)
以上就是 React Suspense 在三種不同架構(gòu)中的四種用例活孩。至于是否一個 API 能夠適用于如此多樣的場景,留待讀者自行判斷乖仇,但希望本文能幫助您更好地理解這個機制憾儒。感謝閱讀询兴!
資源
- React 18 的新 Suspense SSR 架構(gòu)
- 分流工作:使用 Suspense 實現(xiàn)流式服務(wù)端渲染:Shaundai Person
- React.lazy 的 react.dev 文檔和 MDN 動態(tài)導(dǎo)入文檔