React?Server?Component混合式渲染問題詳解
React 官方對 Server Comopnent 是這樣介紹的: zero-bundle-size React Server Components。
這是一種實(shí)驗(yàn)性探索,但相信該探索是個(gè)未來 React 發(fā)展的方向,與 React Server Component 相關(guān)的周邊生態(tài)正在積極的建設(shè)當(dāng)中。
術(shù)語介紹
在 React Server Component (以下稱 Server Component) 推出之后,我們可以簡單的將 React 組件區(qū)分為以下三種:
Server Component | 服務(wù)端渲染組件,擁有訪問數(shù)據(jù)庫、訪問本地文件等能力。無法綁定事件對象,即不擁有交互性。 |
---|---|
Client Component | 客戶端渲染組件,擁有交互性。 |
Share Component | 既可以在服務(wù)端渲染,又可以在客戶端渲染。具體如何渲染取決于誰引入了它。當(dāng)被服務(wù)端組件引入的時(shí)候會由服務(wù)端渲染當(dāng)被客戶端組件引入的時(shí)候會由客戶端渲染。 |
React 官方暫定通過「文件名后綴」來區(qū)分這三種組件:
Server Component
需要以.server.js(/ts/jsx/tsx)
為后綴Client Component
- 需要以
.client.js(/ts/jsx/tsx)
為后綴Share Component
- 以
.js(/ts/jsx/tsx)
為后綴
混合渲染
簡單來說 Server Component 是在服務(wù)端渲染的組件,而 Client Component 是在客戶端渲染的組件。
與類似 SSR , React 在服務(wù)端將 Server Component 渲染好后傳輸給客戶端,客戶端接受到 HTML 和 JS Bundle 后進(jìn)行組件的事件綁定。不同的是:Server Component 只進(jìn)行服務(wù)端渲染,不會進(jìn)行瀏覽器端的 hyration(注水),總的來說頁面由 Client Component 和 Server Component 混合渲染。
這種渲染思路有點(diǎn)像 Islands 架構(gòu),但又有點(diǎn)不太一樣。
如圖:橙色為 Server Component, 藍(lán)色為 Client Component 。
React 是進(jìn)行混合渲染的?
React 官方提供了一個(gè)簡單的 Demo , 通過 Demo,探索一下React sever component的運(yùn)作原理。
渲染入口
瀏覽器請求到 HTML 后,請求入口文件 - main.js, 里面包含了 React Runtime 與 Client Root,Client Root 執(zhí)行創(chuàng)建一個(gè) Context,用來保存客戶端狀態(tài),與此同時(shí),客戶端向服務(wù)端發(fā)出 /react
請求。
// Root.client.jsx 偽代碼 function Root() { const [data, setData] = useState({}); // 向服務(wù)端發(fā)送請求 const componentResponse = useServerResponse(data); return ( <DataContext.Provider value={[data, setData]}> componentResponse.render(); </DataContext.Provider> ); }
看出這里沒有渲染任何真實(shí)的 DOM, 真正的渲染會等 response 返回 Component 后才開始。
請求服務(wù)端組件
Client Root 代碼執(zhí)行后,瀏覽器會向服務(wù)端發(fā)送一個(gè)帶有 data 數(shù)據(jù)的請求,服務(wù)端接收到請求,則進(jìn)行服務(wù)端渲染。
服務(wù)端將從 Server Component Root 開始渲染,一顆混合組件樹將在服務(wù)端渲染成一個(gè)巨大的 VNode。
如圖,這一顆混合組件樹會被渲染成這樣一個(gè)對象,它帶有 React 組件所有必要的信息。
module.exports = { tag: 'Server Root', props: {...}, children: [ { tag: "Client Component1", props: {...}: children: [] }, { tag: "Server Component1", props: {...}: children: [ { tag: "Server Component2", props: {...}: children: [] }, { tag: "Server Component3", props: {...}: children: [] }, ]} ] }
不僅僅是這樣一個(gè)對象, 由于 Client Comopnent 需要 Hydration, React 會將這部分必須要的信息也返回回去。React 最終會返回一個(gè)可解析的 Json 序列 Map。
M1:{"id":"./src/BlogMenu.client.js","chunks":["client0"],"name":"xxx"} J0:["$","main", null, ["]]
- M: 代表 Client Comopnent 所需的 Chunk 信息
- J: 代表 Server Compnent 渲染出的類 react element格式的字符串
React Runtime 渲染
組件數(shù)據(jù)返回給瀏覽器后,React Runtime 開始工作,將返回的 VNode 渲染出真正的 HTML。與此同時(shí),發(fā)出請求,請求 Client Component 所需的 JS Bundle。當(dāng)瀏覽器請求到 Js Bundle 后,React 就可以進(jìn)行選擇性 Hydration(Selective Hydration)。需要注意的是, React 團(tuán)隊(duì)傳輸組件數(shù)據(jù)選擇了流式傳輸,這也意味著 React Runtime 無需等待所有數(shù)據(jù)獲取完后才開始處理數(shù)據(jù)。
啟動流程
- 瀏覽器加載 React Runtime, Client Root 等 js 代碼
- 執(zhí)行 Client Root 代碼,向服務(wù)端發(fā)出請求
- 服務(wù)端接收到請求,開始渲染組件樹
- 服務(wù)端將渲染好的組件樹以字符串的信息返回給瀏覽器
- React Runtime 開始渲染組件且向服務(wù)端請求 Client Component Js Bundle 進(jìn)行選擇性 Hydration(注水)
Client <-> Server 如何通信?
Client Component 與 Server Component 有著天然的環(huán)境隔離,他們是如何互相通信的呢?
Server Component -> Client Component
在服務(wù)端的渲染都是從 Server Root Component 開始的,Server Component 可以簡單的通過 props 向 Client Component 傳遞數(shù)據(jù)。
import ClientComponent from "./ClientComponent"; const ServerRootComponent = () => { return <ClientComponent title="xxx" /> };
但需要注意的是:這里傳遞的數(shù)據(jù)必須是可序列化的,也就是說你無法通過傳遞 Function 等數(shù)據(jù)。
Client Component -> Server Component
Client Component 組件通過 HTTP 向服務(wù)端組件傳輸信息。Server Component 通過 props 的信息接收數(shù)據(jù),當(dāng) Server Component 拿到新的 props 時(shí)會進(jìn)行重新渲染, 之后通過網(wǎng)絡(luò)的手段發(fā)送給瀏覽器,通過 React Runtime 渲染在瀏覽器渲染出最新的 Server Component UI。這也是 Server Component 非常明顯的劣勢:渲染流程極度依賴網(wǎng)絡(luò)。
// Client Component function ClientComponent() { const sendRequest = (props) => { const payload = JSON.stringify(props); fetch(`http://xxxx:8080/react?payload=${payload}`) } return ( <button onclick = {() => sendRequest({ messgae: "something" })} > Click me, send some to server </button> ) } // Serve Component const ServerRootComponent = ({ messgae: "something" }) => { return <ClientComponent title="xxx" /> };
Server Component 所帶來的優(yōu)勢
RSC 推出的背景是 React 官方想要更好的用戶體驗(yàn),更低的維護(hù)成本,更高的性能。通常情況下這三者不能同時(shí)獲得,但 React 團(tuán)隊(duì)覺得「小孩子才做選擇,我全都要」。
根據(jù)官方提出 RFC: React Server Components,可以通過以下幾點(diǎn)能夠看出 React 團(tuán)隊(duì)是如何做到"全都要"的:
更小的 Bundle 體積
通常情況下,我們在前端開發(fā)上使用很多依賴包,但實(shí)際上這些依賴包的引入會增大代碼體積,增加 bundle 加載時(shí)間,降低用戶首屏加載的體驗(yàn)。
例如在頁面上渲染 MarkDown
,我們不得不引入相應(yīng)的渲染庫,以下面的 demo 為例,不知不覺我們引入了 240 kb 的 js 代碼,而且往往這種大型第三方類庫是沒辦法進(jìn)行 tree-shaking。
// NOTE: *before* Server Components import marked from 'marked'; // 35.9K (11.2K gzipped) import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped) function NoteWithMarkdown({text}) { const html = sanitizeHtml(marked(text)); return (/* render */); }
可以想象,為了某一個(gè)計(jì)算任務(wù),我們需要將大型 js 第三方庫傳輸?shù)接脩魹g覽器上,瀏覽器再進(jìn)行解析執(zhí)行它來創(chuàng)造計(jì)算任務(wù)的 runtime, 最后才是計(jì)算。從用戶的角度來講:「我還沒見到網(wǎng)頁內(nèi)容,你就占用了我較大帶寬和 CPU 資源,是何居心」。然而這一切都是可以省去的,我們可以利用 SSR 讓 React 在服務(wù)端先渲染,再將渲染后的 html 發(fā)送給用戶。從這一方面看,Server Component 和 SSR 很類似,但不同的是 SSR 只能適用于首頁渲染,Server Component 在用戶交互的過程中也是服務(wù)端渲染,Server Component 傳輸?shù)囊膊皇?html 文本,而是 json。Server Component 在服務(wù)端渲染好之后會將一段類 React 組件 json 數(shù)據(jù)發(fā)送給瀏覽器,瀏覽器中的 React Runtime 接收到這段 json 數(shù)據(jù) 后,將它渲染成 HTML。
我們舉一個(gè)更加極端的例子:若用戶無交互性組件,所以組件都可以在服務(wù)端渲染,那么所有 UI 渲染都將走「瀏覽器接收"類 react element 文本格式"的數(shù)據(jù),React Runtime 渲染」的形式進(jìn)行渲染。 那么除了一些 Runtime, 我們無需其他 JS Bundle。而 Runtime 的體積是不會隨著項(xiàng)目的增大而增大的,這種常數(shù)系數(shù)級體積也可以稱為 “Zero-Bundle-Size”。因此官方這稱為: “Zero-Bundle-Size Components”。
更好的使用服務(wù)端能力
為了獲取數(shù)據(jù),前端通常需要請求后端接口,這是因?yàn)闉g覽器是沒辦法直接訪問數(shù)據(jù)庫的。但既然我們都借助服務(wù)端的能力了,那我們當(dāng)然可以直接訪問數(shù)據(jù)庫,React 在服務(wù)器上將數(shù)據(jù)渲染進(jìn)組件。
通過自由整合后端能力,我們可以解決:「網(wǎng)絡(luò)往返過多」和「數(shù)據(jù)冗余」問題。甚至我們可以根據(jù)業(yè)務(wù)場景自由地決定數(shù)據(jù)存儲位置,是存儲在內(nèi)存中、還是存儲在文件中或者存儲在數(shù)據(jù)庫。除了數(shù)據(jù)獲取,還可以再開一些"腦洞"。
- 我們可以在 Server Component 的渲染過程中將一些高性能計(jì)算任務(wù)交付給其他語言,如 C++,Rust。
- 這不是必須的,但你可以這么做。…
簡單粗暴一點(diǎn)的說:Nodejs 擁有什么樣的能力,你的組件就能擁有什么能力。
更好的自動化 Code Split
在過去,我們可以通過 React 提供的 lazy + Suspense 進(jìn)行代碼分割。這種方案在某些場景(如 SSR)下無法使用,社區(qū)比較成熟的方案是使用第三方類庫 @loadable
。然而無論是使用哪一種,都會有以下兩個(gè)問題:
- Code Split 需要用戶進(jìn)行手動分割,自行確認(rèn)分割點(diǎn)。
- 與其說是 Code Split,其實(shí)更偏向懶加載。也就是說,只有加載到了代碼切割點(diǎn),我們才會去即時(shí)加載所切割好的代碼。這里還是存在一個(gè)加載等待的問題,削減了code split給性能所帶來的好處。
React核心團(tuán)隊(duì)所提出 Server Component 可以幫助我們解決上面的兩個(gè)問題。
1.React Server Component 將所有 Client Component 的導(dǎo)入視為潛在的分割點(diǎn)。也就是說,你只需要正常的按分模塊思維去組織你的代碼。React 會自動幫你分割
import ClientComponent1 from './ClientComponent1'; function ServerComponent() { return ( <div> <ClientComponent1 /> </div> ) }
2.框架側(cè)可以介入 Server Component 的渲染結(jié)果,因此上層框架可以根據(jù)當(dāng)前請求的上下文來預(yù)測用戶的下一個(gè)動作,從而去「預(yù)加載」對應(yīng)的js代碼。
避免高度抽象所帶來的性能負(fù)擔(dān)
React server component通過在服務(wù)器上的實(shí)時(shí)編譯和渲染,將抽象層在服務(wù)器進(jìn)行剝離,從而降低了抽象層在客戶端運(yùn)行時(shí)所帶來的性能開銷。
舉個(gè)例子,如果一個(gè)組件為了可配置行,被多個(gè) wrapper 包了很多層。但事實(shí)上,這些代碼最終只是渲染為一個(gè)
<div>
。如果把這個(gè)組件改造為 server component 的話,那么我們只需要往客戶端返回一個(gè)<div>
字符串即可。下面例子,我們通過把這個(gè)組件改造為server component,那么,我們大大降低網(wǎng)絡(luò)傳輸?shù)馁Y源大小和客戶端運(yùn)行時(shí)的性能開銷:// Note.server.js// ...imports...function Note({id}) { const note = db.notes.get(id); return <NoteWithMarkdown note={note} />;}// NoteWithMarkdown.server.js// ...imports...function NoteWithMarkdown({note}) { const html = sanitizeHtml(marked(note.text)); return <div ... />;}// client sees:<div> <!-- markdown output here --></div>
參考自:
https://juejin.cn/post/6918602124804915208#heading-5
我們可以通過在 Server Component ,將 HOC 組件進(jìn)行渲染,可能渲染到最后只是一個(gè) <div>
我們就無需將 bundle 傳輸過去,也無需讓瀏覽器消耗性能去渲染。
Sever Component 可能存在的劣勢
弱網(wǎng)情況下的交互體驗(yàn)
如上文所述: React Server Component 的邏輯, 他的渲染流程依靠網(wǎng)絡(luò)。服務(wù)端渲染完畢后將類 React 組件字符串的數(shù)據(jù)傳輸給瀏覽器,瀏覽器中的 Runtime React 再進(jìn)行渲染。顯然,在弱網(wǎng)環(huán)境下,數(shù)據(jù)傳輸會很慢,渲染也會因?yàn)榫W(wǎng)速而推遲,極大的降低了用戶的體驗(yàn)。Server Component 比較難能可貴的是,它跟其他技術(shù)并不是互斥的,而是可以結(jié)合到一塊。例如:我們完全可以將 Server Component 的計(jì)算渲染放在邊緣設(shè)備上進(jìn)行計(jì)算,在一定程度上能給降低網(wǎng)絡(luò)延遲帶來的問題。
開發(fā)者的心智負(fù)擔(dān)
在 React Server Component 推出之后,開發(fā)者在開發(fā)的過程中需要去思考: 「我這個(gè)組件是 Server Component 還是 Client Component」,在這一方面會給開發(fā)者增加額外的心智負(fù)擔(dān),筆者在寫 Demo 時(shí)深有體會,思維上總是有點(diǎn)不習(xí)慣。Nextjs 前一段時(shí)間發(fā)布了 v13,目前已實(shí)現(xiàn)了 Server & Client Component 。參考 Next13 的方案,默認(rèn)情況下開發(fā)者開發(fā)的組件都是 Server Component ,當(dāng)你判斷這個(gè)組件需要交互或者調(diào)用 DOM, BOM 相關(guān) API 時(shí),則標(biāo)記組件為 Client Component。
「默認(rèn)走 Server Component,若有交互需要則走 Client Component」 通過這種原則,相信在一定程度上能給減輕開發(fā)者的心智負(fù)擔(dān)。
應(yīng)用場景: 文檔站
從上面我們可以知道 Server Component 在輕交互性的場景下能夠發(fā)揮它的優(yōu)勢來,輕交互的場景一般我們能想到文檔站。來看一個(gè)小 Demo, 通過這個(gè) Demo 我們觀察到幾個(gè)現(xiàn)象:
- 極小的 Js bundle。
- 文件修改無需 Bundle。
當(dāng)然像文檔站等偏向靜態(tài)的頁面更適合 SSR, SSG,但就像前面所說的它并不與其他的技術(shù)互斥,我們可以將其進(jìn)行結(jié)合,更況且他不僅僅能應(yīng)用于這樣的靜態(tài)場景。
參考文檔
Introducing Zero-Bundle-Size React Server Components
到此這篇關(guān)于React Server Component: 混合式渲染的文章就介紹到這了,更多相關(guān)React Server Component內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react實(shí)現(xiàn)每隔60s刷新一次接口的示例代碼
本文主要介紹了react實(shí)現(xiàn)每隔60s刷新一次接口的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06React 項(xiàng)目中動態(tài)設(shè)置環(huán)境變量
本文主要介紹了React 項(xiàng)目中動態(tài)設(shè)置環(huán)境變量,本文將介紹兩種常用的方法,使用 dotenv 庫和通過命令行參數(shù)傳遞環(huán)境變量,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04使用react實(shí)現(xiàn)手機(jī)號的數(shù)據(jù)同步顯示功能的示例代碼
本篇文章主要介紹了使用react實(shí)現(xiàn)手機(jī)號的數(shù)據(jù)同步顯示功能的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04React useContext與useReducer函數(shù)組件使用
useContext和useReducer 可以用來減少層級使用, useContext,可以理解為供貨商提供一個(gè)公共的共享值,然后下面的消費(fèi)者去接受共享值,只有一個(gè)供貨商,而有多個(gè)消費(fèi)者,可以達(dá)到共享的狀態(tài)改變的目的2023-02-02React在組件中如何監(jiān)聽redux中state狀態(tài)的改變
這篇文章主要介紹了React在組件中如何監(jiān)聽redux中state狀態(tài)的改變,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08React Draggable插件如何實(shí)現(xiàn)拖拽功能
這篇文章主要介紹了React Draggable插件如何實(shí)現(xiàn)拖拽功能問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07