React 渲染的未來,你想知道嗎?
大家好,我是 CUGGZ。
在過去的幾年中,React 的流行度一直在增加,而且還在加速。React 每周的 npm 下載量超過 1400 萬次 ,React Devtools Chrome 擴展有超過 300 萬 的周活躍用戶。
然而,在 React 18 之前,React 中的渲染模式幾乎是相同的。在本文中,我們將研究 React 當前的渲染模式、它們存在的問題,以及 React 18 引入的新模式如何是解決這些問題的。
相關(guān)術(shù)語
在深入研究渲染模式之前,讓我們來看一下將在這篇文章中使用的一些重要術(shù)語:
- Time To First Byte(TTFB) :發(fā)出頁面請求到接收到應(yīng)答數(shù)據(jù)第一個字節(jié)所花費的時間。
- First Paint(FP) :第一個像素對用戶可見的時間。
- First Contentful Paint(FCP): 第一條內(nèi)容可見所需的時間。
- Largest Contentful Paint(LCP) :加載頁面主要內(nèi)容所需的時間。
- Time To Interactive(TTI): 頁面變?yōu)榻换ゲ⒖煽宽憫?yīng)用戶事件的時間。
當前的渲染模式
目前,我們在 React 中使用的最常見的模式就是客戶端渲染和服務(wù)端渲染,以及由 Next.js等框架提供的一些高級形式的服務(wù)端渲染,例如靜態(tài)站點生成(SSG)、增量靜態(tài)生成(ISR)。我們將研究其中這其中的每一個,并深入研究 React 18 引入的新模式。
客戶端渲染(CSR)
在 Next.js 和 Remix 等元框架出現(xiàn)之前,客戶端渲染(主要使用 create-react-app 或其他類似的腳手架)是構(gòu)建 React 應(yīng)用程序的默認方式。
使用客戶端渲染,服務(wù)端只需要為包含必要<script>和<link>標簽的頁面提供基本 HTML。一旦相關(guān)的 JavaScript 下載到瀏覽器。 React 渲染樹并生成所有 DOM 節(jié)點。 路由和數(shù)據(jù)獲取的所有邏輯也由客戶端 JavaScript 處理。
為了看看 CSR 是如何工作的,我們來渲染以下應(yīng)用:
渲染周期如下:
CSR渲染周期.gif
這是客戶端渲染的 Network 圖:
因此,CSR 應(yīng)用接收到應(yīng)答數(shù)據(jù)第一個字節(jié)很快(TTFB),因為它們主要依賴于靜態(tài)資源。 但是,在下載相關(guān) JavaScript 之前,用戶必須盯著空白屏幕。 在那之后,由于大多數(shù)應(yīng)用都需要從 API 獲取數(shù)據(jù)并向用戶顯示相關(guān)數(shù)據(jù),這導(dǎo)致加載頁面主要內(nèi)容所需的時間(LCP)很長。
CSR 的優(yōu)點
- 由于客戶端渲染架構(gòu)包含靜態(tài)文件,因此可以非常輕松地通過 CDN 提供服務(wù)。
- 所有渲染都是在客戶端完成的,因此 CSR 允許我們在不刷新整個頁面的情況下進行導(dǎo)航,從而提供良好的用戶體驗。
- TTFB 時間很快,因此瀏覽器可以立即開始加載字體、CSS 和 JavaScript。
CSR 的缺點
- 由于所有內(nèi)容都在客戶端渲染,因此性能受到很大影響,因為用戶首先需要下載并處理它才能看到頁面上的內(nèi)容。
- 客戶端渲染應(yīng)用通常會在組件掛載時獲取所需的數(shù)據(jù),這會導(dǎo)致糟糕的用戶體驗,因為在初始頁面加載時會遇到很多 loaders。 此外,如果子組件需要獲取數(shù)據(jù),情況可能會變得更糟,這樣它們的父組件獲取完所有數(shù)據(jù)后才會渲染它們,這可能會導(dǎo)致大量 loaders 和糟糕的 Network Waterfall。
- SEO 是客戶端渲染應(yīng)用的一個問題,因為網(wǎng)絡(luò)爬蟲可以輕松讀取服務(wù)端渲染的 HTML,但它們可能不會等待下載完所有 JavaScript 包,執(zhí)行它們并等待客戶端數(shù)據(jù)獲取瀑布流完成 ,這可能會導(dǎo)致不正確的索引。
服務(wù)端渲染(SSR)
目前,在 React 中服務(wù)端渲染的工作方式如下:
- 通過renderToString 獲取相關(guān)數(shù)據(jù)并在服務(wù)端為頁面運行客戶端 JavaScript,這為我們提供了顯示頁面所需的所有 HTML。
- 將此 HTML 提供給客戶端,從而實現(xiàn)快速的 First Contentful Paint。
- 這時還沒有完成, 我們?nèi)匀恍枰螺d并執(zhí)行客戶端 JavaScript 以將 JavaScript 邏輯連接到服務(wù)端生成的 HTML 以使頁面具有交互性(這個過程就是“注水”)。
為了更好地理解它是如何工作的,讓我們來看一下上面例子中使用 SSR 時的生命周期:
渲染周期如下:
SSR渲染周期.gif
這是服務(wù)端渲染的 Network 圖:
因此,使用 SSR,我們可以獲得良好的 FCP 和 LCP,但 TTFB 會受到影響,因為我們必須在服務(wù)端獲取數(shù)據(jù),然后將其轉(zhuǎn)換為 HTML 字符串。
現(xiàn)在,你可能會問這和 Next.js 的 SSG/ISR 有啥區(qū)別呢?它們也必須經(jīng)歷上面的過程。 唯一的區(qū)別是,它們不會受到 TTFB 時間較長的影響。因為 HTML 要么是在構(gòu)建時生成的,要么是在請求傳入時以增量方式生成和緩存的。
但是,SSG/ISR 更適合公共頁面。對于根據(jù)用戶登錄狀態(tài)或瀏覽器上存儲的其他 cookie 更改的頁面,必須使用 SSR。
SSR 的優(yōu)點
- 與 CSR 不同,SEO 要好得多,因為所有 HTML 都是從服務(wù)端預(yù)先生成的,網(wǎng)絡(luò)爬蟲可以毫無問題地爬取它。
- FCP 和 LCP 非???。因此,用戶可以很快看到內(nèi)容,而不是像 CSR 應(yīng)用那樣查看空白屏幕。
SSR 的缺點
- 由于我們在每次請求時首先在服務(wù)端渲染頁面,并且必須等待頁面的數(shù)據(jù)需求,這可能會導(dǎo)致 TTFB 速度變慢。這可能是由多種原因?qū)е碌?,包括未?yōu)化的服務(wù)端代碼或者許多并發(fā)的服務(wù)端請求。不過,使用像 Next.js 這樣的框架可以提前生成頁面并使用 SSG(靜態(tài)站點生成)和 ISR(增量靜態(tài)站點生成)等技術(shù)將它們緩存在服務(wù)端,從而在一定程度上解決了這個問題。
- 即使初始加載速度很快,用戶仍然需要等待下載頁面的所有 JavaScript 并對其進行處理,以便頁面可以重新注水并變得可交互。
新的渲染模式
上面,我們介紹了 React 中當前的渲染模式是什么以及它們存在什么問題。 總結(jié)一下:
- 在 CSR 應(yīng)用中,用戶必須下載所有必要的 JavaScript 并執(zhí)行它以查看/與頁面交互。
- 在 SSR 應(yīng)用中,我們通過在服務(wù)端生成 HTML 來解決了其中的一些問題。然而這并不是最優(yōu)的,因為首先我們必須等待服務(wù)端獲取所有數(shù)據(jù)并生成 HTML。 然后客戶端必須下載整個頁面的 JavaScript。 最后,我們必須執(zhí)行 JavaScript 以連接服務(wù)端生成的 HTML 和 JavaScript 邏輯,以便頁面可以交互。 所以主要問題是我們必須要等待每一步完成,然后才能開始下一步。
React 團隊正在研究一些旨在解決這些問題的新模式。
流式 SSR
瀏覽器可以通過 HTTP 流接收 HTML。流式傳輸允許 Web 服務(wù)端通過單個 HTTP 連接將數(shù)據(jù)發(fā)送到客戶端,該連接可以無限期保持打開狀態(tài)。因此,我們可以通過網(wǎng)絡(luò)以多個塊的形式在瀏覽器上加載數(shù)據(jù),這些數(shù)據(jù)在渲染時按順序加載。
(1)React 18 之前的流式渲染
流式渲染并不是 React 18 中全新的東西。事實上,它從 React 16 開始就存在了。React 16 有一個名為 renderToNodeStream 的方法,與 renderToString 不同,它將前端渲染為瀏覽器的 HTTP 流。
這允許在渲染它的同時以塊的形式發(fā)送 HTML,從而為用戶提供更快的 TTFB 和 LCP,因為初始 HTML 更快地到達瀏覽器。
(2)React 18 中的流式 SSR
React 18 棄用了 renderToNodeStream API,取而代之的是一個名為 renderToPipeableStream 的新 API,它通過 Suspense 解鎖了一些新功能,允許將應(yīng)用分解為更小的獨立單元,這些單元可以獨立完成我們在 SSR 中看到的步驟。這是因為 Suspense 添加了兩個主要功能:
- 服務(wù)端流式渲染。
- 客戶端選擇性注水。
① 服務(wù)端流式渲染
如上所述,React 18 之前的 SSR 是一種全有或全無的方法。 首先,需要獲取頁面所需的數(shù)據(jù),并生成 HTML,然后將其發(fā)送到客戶端。 由于 HTTP 流,情況不再如此。
在 React 18 中想要使用這種方式,可以包裝可能需要較長時間才能加載且在 Suspense 中不需要立即顯示在屏幕上的組件。
為了了解它的工作原理,假設(shè) Comments API 很慢,所以我們將 Comments 組件包裝在Suspense 中:
這樣,初始 HTML 中就不存在 Comments,返回的只有占位的 Spinner:
最后,當數(shù)據(jù)準備好用于服務(wù)端的 Comments 時,React 將發(fā)送最少的 HTML 到帶有內(nèi)聯(lián)<script>標簽的同一流中,以將 HTML 放在正確的位置:
因此,這解決了第一個問題,因為現(xiàn)在不需要等待服務(wù)端獲取所有數(shù)據(jù),瀏覽器可以開始渲染應(yīng)用的其余部分,即使某些部分尚未準備好。
② 客戶端選擇性注水
即使 HTML 被流式傳輸,頁面也不會可交互的,除非頁面的整個 JavaScript 被下載完。這就是選擇性注水的用武之地。
在客戶端渲染期間避免頁面上出現(xiàn)大型包的一種方法就是通過 React.lazy 進行代碼拆分。 它指定了應(yīng)用的某個特定部分不需要同步加載,并且打包工具會將其拆分為單獨的<script>標簽。
React.lazy 的限制是它不適用于服務(wù)端渲染。但在 React 18 中,<Suspense> 除了允許流式傳輸 HTML 之外,它還可以為應(yīng)用的其余部分注水。
所以,現(xiàn)在 React.lazy 在服務(wù)端開箱即用。 當你將 lazy 組件包裹在 <Suspense> 中時,不僅告訴 React 你希望它被流式傳輸,而且即使包裹在 <Suspense> 中的組件仍在被流式傳輸,也允許其余部分注水。這也解決了我們在傳統(tǒng)服務(wù)端渲染中看到的第二個問題。在開始注水之前,不再需要等待所有 JavaScript 下載完畢。
下面,我們把 Comments 包含在 Suspense 中,并使用新的 Suspense 架構(gòu),來看看應(yīng)用的生命周期:
渲染周期如下:
流式 SSR 渲染周期.gif
這就產(chǎn)生了像下面這樣的 Network 圖:
這個例子想說明的是,對于 Suspense,很多連續(xù)發(fā)生的事情現(xiàn)在可以并行發(fā)生。
這不僅有助于我們在 HTML 被流式傳輸后更快地 TTFB,而且用戶不必等待所有 JavaScript 被下載才能開始與應(yīng)用交互。 除此之外,它還有助于在頁面開始流式傳輸時立即加載其他資源(CSS、JavaScript、字體等),有助于并行更多請求。
另外,如果有多個組件包裹在 Suspense 中并且還沒有在客戶端上注水,但是用戶開始與其中一個交互,React 將優(yōu)先考慮給該組件注水。
Server components (Alpha)
上面,我們介紹了如何通過將應(yīng)用分解為更小的單元并分別對它們進行流式處理和選擇性注水來提高服務(wù)端渲染性能。 但是,如果有一種方法可以完全不需要對應(yīng)用的某些部分進行注水呢?
這就是全新的 Server Components RFC 的用武之地。它旨在補充服務(wù)端渲染,允許擁有僅在服務(wù)端渲染且沒有交互性的組件。
它們的工作方式就是可以使用 .server.js/jsx/ts/tsx 擴展創(chuàng)建非交互式服務(wù)端組件,然后它們可以無縫集成并將 props 傳遞給客戶端組件(使用 .client.js/jsx/ts/tsx 擴展),它可以處理頁面的交互部分。以下是它提供的功能的:
(1)不影響客戶端包
服務(wù)端組件僅在服務(wù)端渲染,不需要注水。它允許我們在服務(wù)端渲染靜態(tài)內(nèi)容,同時對客戶端包大小沒有影響。 如果使用的是繁重的庫并且沒有交互性,這可能特別有用,并且它可以完全渲染在服務(wù)端,而不會影響客戶端包。 RFC 中的 Notes 預(yù)覽就是一個很好的例子:
(2)服務(wù)端組件不具有交互性,但可以與客戶端組件組合
由于它們只在服務(wù)端渲染,它們只是接收 props 并渲染視圖的 React 組件。 因此,它們不能像常規(guī)客戶端組件中那樣擁有狀態(tài)、effects 和事件處理程序之類的東西。
盡管它們可以導(dǎo)入具有交互性的客戶端組件,并且在客戶端上渲染時注水,正如我們在普通 SSR 中看到的那樣。 客戶端組件與服務(wù)端組件類似,使用 .client.jsx 或 .client.tsx 后綴定義。
這種可組合性使開發(fā)人員在頁面上節(jié)省大量的包大小,例如具有大部分靜態(tài)內(nèi)容和很少交互元素的詳情頁。 例如:
上面的代碼是服務(wù)端組件如何與客戶端組件組合的示例。 讓我們來分解一下:
- Post 服務(wù)端組件主要包含靜態(tài)數(shù)據(jù),包括文章標題、內(nèi)容和發(fā)布日期。
- 由于服務(wù)端組件不能有任何交互性,我們導(dǎo)入了一個名為 AddComment 的客戶端組件,它允許用戶添加評論。
這里,我們在服務(wù)端組件中導(dǎo)入的所有日期和 markdown 解析庫都不會在客戶端下載。我們在客戶端下載的唯一 JavaScript 就是 AddComment 組件。
(3)服務(wù)端組件可以直接訪問后端
由于它們僅在服務(wù)端渲染,因此可以使用它們直接從組件訪問數(shù)據(jù)庫和其他僅限后端的數(shù)據(jù)源,如下所示:
現(xiàn)在你可能會說,在傳統(tǒng)的服務(wù)端渲染中也可以實現(xiàn)這一點。 例如,Next.js 可以直接在 getServerSideProps 和 getStaticProps 中訪問服務(wù)端數(shù)據(jù)。 沒錯,但區(qū)別在于,傳統(tǒng)的 SSR 是一種全有或全無的方法,只能在頂級頁面上完成,但服務(wù)端組件可以在每個組件的基礎(chǔ)上執(zhí)行此操作。
(4)自動代碼拆分
代碼拆分是一個概念,它允許將應(yīng)用分成更小的塊,向客戶端發(fā)送更少的代碼。對應(yīng)用進行代碼拆分的最常見方式就是按路由進行拆分。這也是 Next.js 等框架默認拆分包的方式。
除了自動代碼拆分之外,React 還允許使用 React.lazy API 在運行時延遲加載不同的模塊。 這又是一個來自 RFC 的很好的例子,說明這可能特別有用:
這種技術(shù)通過在運行時只動態(tài)導(dǎo)入需要的組件來提高性能,但它確實有一些問題。 例如,這種方法會延遲應(yīng)用開始加載代碼的時間,從而抵消了加載更少代碼的好處。
正如我們之前在客戶端組件如何與服務(wù)器組件組合中看到的那樣,它們通過將所有客戶端組件導(dǎo)入視為潛在的代碼拆分點,并允許開發(fā)人員選擇要在服務(wù)端更早渲染的內(nèi)容,從而使客戶端能夠更早下載。 下面是 RFC 中使用服務(wù)端組件的相同 PhotoRenderer 示例:
服務(wù)端組件可以在保留客戶端狀態(tài)的同時重新加載:我們可以隨時從客戶端重新獲取服務(wù)端樹,以從服務(wù)端獲取更新的狀態(tài),而不會破壞本地客戶端狀態(tài)、焦點甚至正在進行的動畫。
這是可能的,因為接收到的 UI 描述是數(shù)據(jù)而不是純 HTML,這允許 React 將數(shù)據(jù)合并到現(xiàn)有組件中,從而使客戶端狀態(tài)不會被破壞。
(5)服務(wù)端組件與 Suspense 集成
服務(wù)器組件可以通過 <Suspense> 逐步流式傳輸,正如在上面中看到的那樣,這允許我們在等待頁面剩余部分加載時創(chuàng)建加載狀態(tài)并快速顯示重要內(nèi)容。
接下來看看上面的例子在使用 React 服務(wù)端組件時是什么樣的。 這次 Sidebar 和 Post 是服務(wù)端組件,而 Navbar 和 Comments 是客戶端組件。 我們也將 Post 包裹在 Suspense 中。
渲染周期如下:
Server components 渲染周期.gif
它的 Network 圖與使用 Suspense 的流式渲染非常相似,但 JavaScript 更少。因此,服務(wù)端組件甚至是解決我們一開始的問題的進一步措施,它不僅可以下載更少的 JavaScript,而且還顯著改善了開發(fā)者體驗。
React 團隊還在 RFC 常見問題解答中提到,他們在 Facebook 的單個頁面上對少數(shù)用戶進行了實驗,產(chǎn)品代碼大小減少了約 30%。
什么時候可以開始使用這些功能?
目前,服務(wù)端組件仍處于 alpha 階段,而具有新 Suspense 架構(gòu)的流式 SSR 所需的用于數(shù)據(jù)獲取的 Suspense 還沒有正式發(fā)布,將在 React 18 的小更新中發(fā)布。
相關(guān)演示
在這里查看 React 團隊和 Next.js 團隊的演示:
- 使用新的 Suspense 架構(gòu)演示流式傳輸 SSR:https://codesandbox.io/s/kind-sammet-j56ro。
- 服務(wù)端組件演示:https://github.com/reactjs/server-components-demo。
- Next.js 服務(wù)端組件演示:https://github.com/vercel/next-react-server-components。