Why React Server Components Are Breaking Builds to Win Tomorrow 阅读笔记
type
status
date
Mar 5, 2024 08:13 AM
slug
summary
tags
category
icon
password
总结
React生态下前端UI呈现技术的演进过程
Client-side rendering (CSR)
传统SPA中使用的技术,页面的所有UI完全交由客户端渲染。
大概的渲染流程:
CSR的缺点:
- SEO不友好,服务端返回的html中一般只包含一个应用的挂载点div
- 客户端执行js生成UI的过程耗时长,用户需要等待
Server-side Rendering (SSR)
基于上述CSR中提到的缺点,提出了SSR的技术,即:HTML的完整文档结构完全交由服务端渲染生成并返回给客户端。
大概的渲染流程:
SSR同时解决了CSR的两个缺点,服务端返回的html对SEO非常友好,且减少了用户看到最终UI的时间。
SSR过程中有一个关键步骤称为
Hydration (水合)
,主要包括:- 基于服务端的静态HTML在内存中重新构建组件树
- 初始化应用状态
- 在对应的DOM元素上绑定交互逻辑,如:点击事件
还有一个与SSR差不多的技术,称为
Static Site Generation (SSG)
。二者最大的不同在于服务端渲染HTML的时机:
- SSG - 应用构建时预先生成,一般适用于内容基本不变的页面,如:博客文章
- SSR - 用户请求时即时生成,一般适用于根据不同的标识动态生成不同内容的场景,如:社交动态
当然,SSR也有缺点:
- 服务端渲染前,页面所需的所有数据要提前获取到,这也会增加用户的等待时间
- 水合过程要求客户端的组件树与服务端的组件树完全一致,这就导致了水合过程必须要等到对应的JS加载完毕才能开始进行
- 水合过程启动后无法暂停,页面的所有组件要在一次水合中全部处理完毕后,用户才能开始交互
Suspense for Server-side Rendering
基于上述SSR中提到的缺点,React18提出了另一个优化方案 —— Suspense。
Suspense新增了两个能力:
- HTML流式传输
(HTML streaming)
- 客户端选择性水合
(Selective hydration)
HTML流式传输的大概流程:
首次渲染:使用Suspense包裹的组件,在服务端渲染过程中会被忽略,取而代之的是一个占位组件(一般是加载提示),渲染完成后服务端返回携带占位的HTML
后续渲染:当包裹组件的数据准备就绪时,服务端会完成后续的HTML渲染,然后再次返回HTML(通过
inline script
计算对应的占位位置)客户端选择性水合的大概流程:
刚才提到,传统SSR中水合过程要等待页面的JS加载完成后才能开始,而通过代码分割
(code splitting)
则可以实现按需水合。针对被Suspense包裹的组件对应的JS代码做分割处理,可以提前水合已准备好的组件,等到后续的JS加载完成后继续进行后续的水合,这样可以尽快使应用”活”起来。
更进一步,React支持动态调整组件水合的优先级,通过在事件捕获阶段的监听,可以优先水合正在被用户交互的组件,改善用户体验。(过程动图查看原文即可,此处不再赘述)
Suspense SSR的缺点及提出的疑问:
- 虽说Suspense支持JS代码动态加载,但是客户端最终要加载的代码总量是不变的。随着功能的增加,代码总量也会越来越多,是否所有代码都是客户端需要的?
- 水合过程只在客户端进行,对于无交互的组件是否有必要?
- 所有JS逻辑都在客户端中进行,对客户端设备有一定性能要求,是否可以利用一下服务端更强大的处理能力?
React Server Components (RSC)
基于上面针对Suspense SSR提出的几个疑问,React推出了RSC的架构,旨在最大程度上同时利用服务端与客户端进行UI呈现。
RSC作为一种新的渲染架构,引入了双组件模型,分别为:
- Client Components
- Server Components
Client Components:
与讨论RSC之前所说的普通React组件一样,这些组件基本在客户端渲染 (CSR),但同时也支持在服务端SSR时渲染一次到HTML中,这些组件可以访问客户端环境,如:state、effects、事件监听/委托、geolocation以及localStorage等。
关于Client Components更多细节参考Next.js文档。
Server Components:
RSC中新提出的一种组件类型。这些组件只在服务端渲染执行,不会下载到客户端中。
带来的好处有:
- 零bundle文件大小,客户端不需要下载这部分组件的代码
- 支持直接访问服务端资源,如数据库、文件系统
- 数据获取更高效,没有了客户端请求服务端的过程消耗或者可能存在的串行请求行为
- 服务端层缓存,在后续的请求或不同的用户中可以复用
- 首屏加载速度快以及FCP得分高,服务端直出最终页面
- 与SSR一样,对SEO友好
- 高效的streaming,支持分chunk渲染,用户可以尽快看到页面
关于Server Components更多细节参考Next.js文档。
RSC首次加载的渲染生命周期:
- 浏览器请求页面,Next.js App Router根据URL匹配对应的Server Components (SC),通知React开始渲染
- React渲染SC及其所有子SC,结果转换为一种特殊的JSON格式,称为RSC payload;检测到Suspend包裹的组件时,会使用占位符替代
- 同时,Client Components (CC) 开始准备后续要水合要用到的指令 (instructions)
- Next.js 在服务端使用RSC Payload与CC水合指令生成HTML,并流式返回给用户
- 同时,Next.js 也会流式返回RSC Payload
- 客户端接收到响应后,使用RSC Payload与水合指令进行渐进式 (progressively) UI渲染
- 所有SC与CC的渲染完成后,用户看到最终界面
- CC进行水合,激活组件的交互逻辑
RSC更新的渲染生命周期:
- 浏览器重新请求指定UI,如:路由刷新
- Next.js 处理请求,匹配对应的SC,通知React渲染组件树,与之前流程一样得到RSC Payload与水合指令
- 不生成HTML,直接流式返回RSC Payload给客户端
- 客户端接收到新的payload后,开始重新渲染
- React对比调和(reconcile)前后两次渲染结果(JSON比较),在保持原组件状态的基础上,更新DOM节点
碎碎念
个人用下来,体验并没有想象中好,不仅带来了更多的心智负担,也增加了生态上第三方库适配的工作。虽说的确解决了一些问题,但是带来了更多的问题。
都说Next.js推RSC是出于商业化考虑(Vercel集成),也可以理解吧,纯粹的技术向企业基本不可能存在。
或许,后续可以尝试一下Remix,据说写起来比Next.js舒服太多~