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