作者:谢奇璇

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 官方暂定通过「文件名后缀」来区分这三种组件:

  1. Server Component 需要以 .server.js(/ts/jsx/tsx) 为后缀
  2. Client Component 需要以 .client.js(/ts/jsx/tsx) 为后缀
  3. 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 是进行混合渲染的?

React 官方提供了一个简单的 Demo , 通过 Demo,探索一下React sever component的运作原理。

渲染入口

浏览器请求到 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>
    );
}

看出这里没有渲染任何真实的 DOM, 真正的渲染会等 response 返回 Component 后才开始。

请求服务端组件

Client Root 代码执行后,浏览器会向服务端发送一个带有 data 数据的请求,服务端接收到请求,则进行服务端渲染。

服务端将从 Server Component Root 开始渲染,一颗混合组件树将在服务端渲染成一个巨大的 VNode。

如图,这一颗混合组件树会被渲染成这样一个对象,它带有 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: [] },
        ]}
    ]
}

不仅仅是这样一个对象, 由于 Client Comopnent 需要 Hydration, React 会将这部分必须要的信息也返回回去。React 最终会返回一个可解析的 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 渲染

组件数据返回给浏览器后,React Runtime 开始工作,将返回的 VNode 渲染出真正的 HTML。与此同时,发出请求,请求 Client Component 所需的 JS Bundle。当浏览器请求到 Js Bundle 后,React 就可以进行选择性 Hydration(Selective Hydration)。需要注意的是, React 团队传输组件数据选择了流式传输,这也意味着 React Runtime 无需等待所有数据获取完后才开始处理数据。

启动流程

  1. 浏览器加载 React Runtime, Client Root 等 js 代码
  2. 执行 Client Root 代码,向服务端发出请求
  3. 服务端接收到请求,开始渲染组件树
  4. 服务端将渲染好的组件树以字符串的信息返回给浏览器
  5. React Runtime 开始渲染组件且向服务端请求 Client Component Js Bundle 进行选择性 Hydration(注水)

Client <-> Server 如何通信?

Client Component 与 Server Component 有着天然的环境隔离,他们是如何互相通信的呢?

Server Component -> Client Component

在服务端的渲染都是从 Server Root Component 开始的,Server Component 可以简单的通过 props 向 Client Component 传递数据。

import ClientComponent from "./ClientComponent";

const ServerRootComponent = () => {
    return <ClientComponent title="xxx" />
};

但需要注意的是:这里传递的数据必须是可序列化的,也就是说你无法通过传递 Function 等数据。

Client Component  -> Server Component

Client Component 组件通过 HTTP  向服务端组件传输信息。Server Component 通过 props 的信息接收数据,当 Server Component 拿到新的 props 时会进行重新渲染, 之后通过网络的手段发送给浏览器,通过 React Runtime 渲染在浏览器渲染出最新的 Server Component UI。这也是 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,可以通过以下几点能够看出 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 */);
}

可以想象,为了某一个计算任务,我们需要将大型 js 第三方库传输到用户浏览器上,浏览器再进行解析执行它来创造计算任务的 runtime, 最后才是计算。从用户的角度来讲:「我还没见到网页内容,你就占用了我较大带宽和 CPU 资源,是何居心」。然而这一切都是可以省去的,我们可以利用 SSR 让 React 在服务端先渲染,再将渲染后的 html 发送给用户。从这一方面看,Server Component 和 SSR 很类似,但不同的是 SSR 只能适用于首页渲染,Server Component 在用户交互的过程中也是服务端渲染,Server Component 传输的也不是 html 文本,而是 json。Server Component 在服务端渲染好之后会将一段类 React 组件 json 数据发送给浏览器,浏览器中的 React Runtime 接收到这段 json  数据 后,将它渲染成 HTML。

我们举一个更加极端的例子:若用户无交互性组件,所以组件都可以在服务端渲染,那么所有 UI 渲染都将走「浏览器接收"类 react element 文本格式"的数据,React Runtime 渲染」的形式进行渲染。 那么除了一些 Runtime, 我们无需其他 JS Bundle。而 Runtime 的体积是不会随着项目的增大而增大的,这种常数系数级体积也可以称为 "Zero-Bundle-Size"。因此官方这称为: "Zero-Bundle-Size Components"。

更好的使用服务端能力

为了获取数据,前端通常需要请求后端接口,这是因为浏览器是没办法直接访问数据库的。但既然我们都借助服务端的能力了,那我们当然可以直接访问数据库,React 在服务器上将数据渲染进组件。

通过自由整合后端能力,我们可以解决:「网络往返过多」和「数据冗余」问题。甚至我们可以根据业务场景自由地决定数据存储位置,是存储在内存中、还是存储在文件中或者存储在数据库。除了数据获取,还可以再开一些"脑洞"。

  • 我们可以在 Server Component 的渲染过程中将一些高性能计算任务交付给其他语言,如 C++,Rust。这不是必须的,但你可以这么做。
  • ......

简单粗暴一点的说:Nodejs 拥有什么样的能力,你的组件就能拥有什么能力。

更好的自动化 Code Split

在过去,我们可以通过 React 提供的 lazy + Suspense 进行代码分割。这种方案在某些场景(如 SSR)下无法使用,社区比较成熟的方案是使用第三方类库 @loadable 。然而无论是使用哪一种,都会有以下两个问题:

  1. Code Split 需要用户进行手动分割,自行确认分割点。
  2. 与其说是 Code Split,其实更偏向懒加载。也就是说,只有加载到了代码切割点,我们才会去即时加载所切割好的代码。这里还是存在一个加载等待的问题,削减了code split给性能所带来的好处。

React核心团队所提出 Server Component 可以帮助我们解决上面的两个问题。

  1. React Server Component 将所有 Client Component 的导入视为潜在的分割点。也就是说,你只需要正常的按分模块思维去组织你的代码。React 会自动帮你分割
import ClientComponent1 from './ClientComponent1';

function ServerComponent() {
    return (
        <div>
            <ClientComponent1 />
        </div>
    )
}
  1. 框架侧可以介入 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 可能存在的劣势

弱网情况下的交互体验

如上文所述: React Server Component 的逻辑, 他的渲染流程依靠网络。服务端渲染完毕后将类 React 组件字符串的数据传输给浏览器,浏览器中的 Runtime React 再进行渲染。显然,在弱网环境下,数据传输会很慢,渲染也会因为网速而推迟,极大的降低了用户的体验。Server Component 比较难能可贵的是,它跟其他技术并不是互斥的,而是可以结合到一块。例如:我们完全可以将 Server Component 的计算渲染放在边缘设备上进行计算,在一定程度上能给降低网络延迟带来的问题。

开发者的心智负担

在 React Server Component 推出之后,开发者在开发的过程中需要去思考: 「我这个组件是 Server Component 还是 Client Component」,在这一方面会给开发者增加额外的心智负担,笔者在写 Demo 时深有体会,思维上总是有点不习惯。Nextjs 前一段时间发布了 v13,目前已实现了 Server & Client Component 。参考 Next13 的方案,默认情况下开发者开发的组件都是 Server Component ,当你判断这个组件需要交互或者调用 DOM, BOM 相关 API 时,则标记组件为 Client Component。

「默认走 Server Component,若有交互需要则走 Client Component」 通过这种原则,相信在一定程度上能给减轻开发者的心智负担。

应用场景: 文档站

从上面我们可以知道 Server Component 在轻交互性的场景下能够发挥它的优势来,轻交互的场景一般我们能想到文档站。来看一个小 Demo, 通过这个 Demo 我们观察到几个现象:

  1. 极小的 Js bundle。
  2. 文件修改无需 Bundle。

当然像文档站等偏向静态的页面更适合 SSR, SSG,但就像前面所说的它并不与其他的技术互斥,我们可以将其进行结合,更况且他不仅仅能应用于这样的静态场景。

参考文档

React Server Component: 混合式渲染的更多相关文章

  1. React server rendering —— 网易美学主站同构实录

    此文已由作者张硕授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 网易美学主站在最初开发时,因为各种历史原因,引入了例如JQuery,Bootstrop,Angular, Re ...

  2. React 在服务端渲染的实现

    原文地址:Server-Side React Rendering 原文作者:Roger Jin 译者:牧云云 React 在服务端渲染的实现 React是最受欢迎的客户端 JavaScript 框架, ...

  3. [译]React 在服务端渲染的实现

    原文地址:Server-Side React Rendering 原文作者:Roger Jin React 在服务端渲染的实现 React是最受欢迎的客户端 JavaScript 框架,但你知道吗(可 ...

  4. 【译】在React中实现条件渲染的7种方法

    原文地址:https://scotch.io/tutorials/7-ways-to-implement-conditional-rendering-in-react-applications 借助R ...

  5. React Canvas:高性能渲染 React 组

    React Canvas 提供了使用 Canvas 渲染移动 Web App 界面的能力,替代传统的 DOM 渲染,具有更接近 Native App 的使用体验.React Canvas 提供了一组标 ...

  6. 腾讯优测优分享 | 探索react native首屏渲染最佳实践

    腾讯优测是专业的移动云测试平台,旗下的优分享不定时提供大量移动研发及测试相关的干货~ 此文主要与以下内容相关,希望对大家有帮助. react native给了我们使用javascript开发原生app ...

  7. 探索react native首屏渲染最佳实践

    文 / 腾讯 龚麒 0.前言 react native给了我们使用javascript开发原生app的能力,在使用react native完成兴趣部落安卓端发现tab改造后,我们开始对由react n ...

  8. The required Server component failed to start so Tomcat is unable to start问题解决

    问题出现: Server Tomcat v8.5 Server at localhost failed to start.  或者The required Server component faile ...

  9. React & styled component

    React & styled component https://www.styled-components.com/#your-first-styled-component tagged t ...

  10. React 16 服务端渲染的新特性

    React 16 服务端渲染的新特性 React 16 中关于服务端渲染的新特性 快速介绍React 16 服务端渲染的新特性,包括数组.性能.流等 React 16 终于来了!

随机推荐

  1. JS数据结构之 Map

    JS数据结构之 Map Map介绍 Map(映射)是ES6引入的一种数据结构.这是一种存储键值对列表很方便的方法,类似于其他编程语言的哈希表. HashMap(哈希表),也叫做散列表.是根据关键码值 ...

  2. 【一月一本技术书】-【Go语言设计与实现】- 9月

    Go : 2009.11.10 代表作:Docker.k8s.etcd 模仿C语言,目标:互联网的C语言 讲的晦涩难懂....硬板..放弃了好几次才读完.满分10分,打6分. 下个月:Python数据 ...

  3. Elastic:为Elastic Docker部署设置安全

    文章转载自:https://elasticstack.blog.csdn.net/article/details/105710973 创建docker-compose.yml 在之前的教程中,那里使用 ...

  4. 在 CentOS 8/RHEL 8 上安装和使用 Cockpit

    Cockpit 是一个基于 Web 的服务器管理工具,可用于 CentOS 和 RHEL 系统.最近发布的 CentOS 8 和 RHEL 8,其中 cockpit 是默认的服务器管理工具.它的软件包 ...

  5. 线程池底层原理详解与源码分析(补充部分---ScheduledThreadPoolExecutor类分析)

    [1]前言 本篇幅是对 线程池底层原理详解与源码分析  的补充,默认你已经看完了上一篇对ThreadPoolExecutor类有了足够的了解. [2]ScheduledThreadPoolExecut ...

  6. 洛谷P2216 HAOI2007 理想的正方形 (单调队列)

    题目就是要求在n*m的矩形中找出一个k*k的正方形(理想正方形),使得这个正方形内最值之差最小(就是要维护最大值和最小值),显然我们可以用单调队列维护. 但是二维平面上单调队列怎么用? 我们先对行处理 ...

  7. MSQL-->存储引擎

    概述 MySQL体系结构图 Innodb引擎是在mysql的5.5版本之后的默认存储引擎. Index是在引擎层次的,不同的存储引擎index的用法不同. 存储引擎就是存储数据,建立索引,更新查询数据 ...

  8. 华为路由器OSPF配置常用命令

    OSPF单区域配置 ospf 1 进入ospf视图1代表进程号 area 0 创建区域并进入OSPF区域视图,输入要创建的区域ID,骨干区域即区域0 network +IP +匹配码 display ...

  9. python基础作业1

    目录 附加练习题(提示:一步步拆解) 1.想办法打印出jason 2.想办法打印出大宝贝 3.想办法打印出run 4.获取用户输入并打印成下列格式 5 根据用户输入内容打印其权限 6 编写用户登录程序 ...

  10. 京东云TiDB SQL优化的最佳实践

    京东云TiDB SQL层的背景介绍 从总体上概括 TiDB 和 MySQL 兼容策略,如下表: SQL层的架构 用户的 SQL 请求会直接或者通过 Load Balancer 发送到 京东云TiDB ...