React Server Component: 混合式渲染
React 官方对 Server Comopnent 是这样介绍的: zero-bundle-size React Server Components。
这是一种实验性探索,但相信该探索是个未来 React 发展的方向,与 React Server Component 相关的周边生态正在积极的建设当中。
术语介绍
在 React Server Component (以下称 Server Component) 推出之后,我们可以简单的将 React 组件区分为以下三种:
Server Component | 服务端渲染组件,拥有访问数据库、访问本地文件等能力。无法绑定事件对象,即不拥有交互性。 |
---|---|
Client Component | 客户端渲染组件,拥有交互性。 |
Share Component | 既可以在服务端渲染,又可以在客户端渲染。具体如何渲染取决于谁引入了它。当被服务端组件引入的时候会由服务端渲染当被客户端组件引入的时候会由客户端渲染。 |
React 官方暂定通过「文件名后缀」来区分这三种组件:
Server Component
需要以.server.js(/ts/jsx/tsx)
为后缀Client Component
需要以.client.js(/ts/jsx/tsx)
为后缀Share Component
以.js(/ts/jsx/tsx)
为后缀
混合渲染
简单来说 Server Component 是在服务端渲染的组件,而 Client Component 是在客户端渲染的组件。
与类似 SSR , React 在服务端将 Server Component 渲染好后传输给客户端,客户端接受到 HTML 和 JS Bundle 后进行组件的事件绑定。不同的是:Server Component 只进行服务端渲染,不会进行浏览器端的 hyration(注水),总的来说页面由 Client Component 和 Server Component 混合渲染。
这种渲染思路有点像 Islands 架构,但又有点不太一样。
如图:橙色为 Server Component, 蓝色为 Client Component 。
React 是进行混合渲染的?
渲染入口
浏览器请求到 HTML 后,请求入口文件 - main.js, 里面包含了 React Runtime 与 Client Root,Client Root 执行创建一个 Context,用来保存客户端状态,与此同时,客户端向服务端发出 /react
请求。
// Root.client.jsx 伪代码
function Root() {
const [data, setData] = useState({});
// 向服务端发送请求
const componentResponse = useServerResponse(data);
return (
<DataContext.Provider value={[data, setData]}>
componentResponse.render();
</DataContext.Provider>
);
}
请求服务端组件
Client Root 代码执行后,浏览器会向服务端发送一个带有 data 数据的请求,服务端接收到请求,则进行服务端渲染。
服务端将从 Server Component Root 开始渲染,一颗混合组件树将在服务端渲染成一个巨大的 VNode。
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: [] },
]}
]
}
M1:{"id":"./src/BlogMenu.client.js","chunks":["client0"],"name":"xxx"}
J0:["$","main", null, ["]]
M: 代表 Client Comopnent 所需的 Chunk 信息 J: 代表 Server Compnent 渲染出的类 react element格式的字符串
React Runtime 渲染
启动流程
浏览器加载 React Runtime, Client Root 等 js 代码 执行 Client Root 代码,向服务端发出请求 服务端接收到请求,开始渲染组件树 服务端将渲染好的组件树以字符串的信息返回给浏览器 React Runtime 开始渲染组件且向服务端请求 Client Component Js Bundle 进行选择性 Hydration(注水)
Client <-> Server 如何通信?
Server Component -> Client Component
import ClientComponent from "./ClientComponent";
const ServerRootComponent = () => {
return <ClientComponent title="xxx" />
};
但需要注意的是:这里传递的数据必须是可序列化的,也就是说你无法通过传递 Function 等数据。
Client Component -> Server Component
// 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 所带来的优势
RSC 推出的背景是 React 官方想要更好的用户体验,更低的维护成本,更高的性能。通常情况下这三者不能同时获得,但 React 团队觉得「小孩子才做选择,我全都要」。
根据官方提出 RFC: React Server Components( https://github.com/josephsavona/rfcs/blob/server-components/text/0188-server-components.md ),可以通过以下几点能够看出 React 团队是如何做到"全都要"的:
更小的 Bundle 体积
通常情况下,我们在前端开发上使用很多依赖包,但实际上这些依赖包的引入会增大代码体积,增加 bundle 加载时间,降低用户首屏加载的体验。
例如在页面上渲染 MarkDown
,我们不得不引入相应的渲染库,以下面的 demo 为例,不知不觉我们引入了 240 kb 的 js 代码,而且往往这种大型第三方类库是没办法进行 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 */);
}
更好的使用服务端能力
为了获取数据,前端通常需要请求后端接口,这是因为浏览器是没办法直接访问数据库的。但既然我们都借助服务端的能力了,那我们当然可以直接访问数据库,React 在服务器上将数据渲染进组件。
我们可以在 Server Component 的渲染过程中将一些高性能计算任务交付给其他语言,如 C++,Rust。这不是必须的,但你可以这么做。 ......
更好的自动化 Code Split
@loadable
。然而无论是使用哪一种,都会有以下两个问题:Code Split 需要用户进行手动分割,自行确认分割点。 与其说是 Code Split,其实更偏向懒加载。也就是说,只有加载到了代码切割点,我们才会去即时加载所切割好的代码。这里还是存在一个加载等待的问题,削减了code split给性能所带来的好处。
React Server Component 将所有 Client Component 的导入视为潜在的分割点。也就是说,你只需要正常的按分模块思维去组织你的代码。React 会自动帮你分割
import ClientComponent1 from './ClientComponent1';
function ServerComponent() {
return (
<div>
<ClientComponent1 />
</div>
)
}
框架侧可以介入 Server Component 的渲染结果,因此上层框架可以根据当前请求的上下文来预测用户的下一个动作,从而去「预加载」对应的js代码。
避免高度抽象所带来的性能负担
React server component通过在服务器上的实时编译和渲染,将抽象层在服务器进行剥离,从而降低了抽象层在客户端运行时所带来的性能开销。
举个例子,如果一个组件为了可配置行,被多个 wrapper 包了很多层。但事实上,这些代码最终只是渲染为一个
<div>
。如果把这个组件改造为 server component 的话,那么我们只需要往客户端返回一个<div>
字符串即可。下面例子,我们通过把这个组件改造为server component,那么,我们大大降低网络传输的资源大小和客户端运行时的性能开销:// 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 组件进行渲染,可能渲染到最后只是一个 <div>
我们就无需将 bundle 传输过去,也无需让浏览器消耗性能去渲染。
Sever Component 可能存在的劣势
弱网情况下的交互体验
开发者的心智负担
「默认走 Server Component,若有交互需要则走 Client Component」 通过这种原则,相信在一定程度上能给减轻开发者的心智负担。
应用场景: 文档站
极小的 Js bundle。 文件修改无需 Bundle。
参考文档
【react】初探server component(https://juejin.cn/post/6918602124804915208) Introducing Zero-Bundle-Size React Server Components(https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html) RFC: React Server Components (https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md)