原文链接:Understanding React Server Components
原文作者:Alice Alexandra Moore
前言及简单介绍:Vercel(Next.js背后公司)关于最新的React Server Components(React服务端组件)的介绍,以下简称RSC,相对CSR及SSR(SSR实践可参考我之前的文章)具有更细粒度的组件渲染方式,目前来体验RSC的最便捷方式还是用Next.js。
以下为译文:
副标题:学习React服务端组件的基础以更好的理解为什么(及何时)应该应用至项目中。
React Server Components (RSCs)有点颠覆了React作为一个纯粹的UI渲染库的定位,它开始涉及在框架内部处理数据获取及远程客户端-服务端通信的领域。
接下来,我们将带你了解为什么要创造开发React服务端组件,它主要带来哪些好处,及应该在何时去使用它们。我们也会顺便涉及一些Next.js中通过App Router
(Next.js 13.4)是如何简化RSC的使用及增强RSC的实现细节的。
为什么我们需要React服务端组件?
大家回顾下在React出现前的开发环境。通过像PHP这样的语言,我们在客户端与服务端之间有着很紧密的联系。在一个大而单一的系统架构中,你可以直接在你正在开发的页面中调用服务端的能力来获取数据。然而,它们也有自己的弱点,比如命名上的缺点,对于一个大项目的扩展性问题,跨团队依赖及高并发的需求。
React的创建初衷就是为了可组合性以及对于已有项目的渐进式采用。正是为了回应大家对于页面丰富的可交互性的诉求,React把客户端与服务端解耦,使得前端变得可以更加灵活的去组装页面。这对于多团队来说至关重要:2个React组件分别由不同的团队开发,无需额外处理就可以相互运行,因为他们都运行在同一个框架内。
为了实现这样的目标,React需要在现有Web标准上做创新。通过过去十年在多页应用(MPA)及单页应用(SPA)之间以及客户端渲染与服务端渲染之间的相互进化,我们依然不忘初心:更快的服务提供数据,提供丰富的可交互性,以及维护良好的开发体验。
服务端渲染(SSR)和 React Suspense解决了什么?
从最开始发展至我们现在所处的现状-有了服务端组件,在这之前还有其他关键问题需要解决。为了更好的理解RSCs的必要性,我们需要先去了解为什么我们需要服务端渲染(SSR)及React Suspense。
SSR聚焦在初始页面加载,把预渲染的html传到浏览器,然后在它变成一个典型的react应用之前,浏览器需要下载必需的JavaScript, 然后把服务端返回的html进行水合化(rehydrate),最后才能变成可交互的应用。SSR也只会发生在一种情况下:直接导航到一个页面的时候(打开或刷新一个页面)。
仅仅通过SSR,用户获取HTML的速度是更快了,但是在页面可以用js交互之前必须等待"要么全部要么没有"的请求瀑布流:
- 在页面显示任何东西之前,所有的数据都需要在服务端获取到
- 在客户端水合之前,浏览器需要从服务端下载所有的JavaScript
- 在用户可以交互之前,客户端需要完成所有的水合作用
为了解决这个问题,React开发了Suspense,使得服务端可以以流的形式传输HTML,并且在客户端可以做部分水合。通过把一个组件包裹在<Suspense>
下,你可以告诉服务端来降低这个组件渲染及水合化的优先级,让优先级高的组件优先加载而不被其他繁重的组件给阻塞。
当你有多个组件包裹在<Suspense>
里时,React会以你在组件树中写的顺序处理下去,允许你在应用中以最优化的流的形式进行传输。然而如果用户尝试在某个组件上进行交互时,被交互的组件将具有比其他组件更高的优先级。
这种方式大大优化了之前的问题,但还有些尚待解决的问题:
- 在页面显示任何UI之前整个页面的数据需要在服务端都获取到。另一种方式就是在客户端的
useEffect()
hook里发请求,这种方式的问题是相较于服务端直接获取数据这里多了额外的网络请求,并且这个只会在组件渲染及水合之后发生 - 所有的js代码最终都会被下载,即使是异步推流到客户端的。当应用变得越来越复杂时,用户下载的代码也会变得更多
- 尽管优化了水合(hydration)流程,用户依然不能与相应的组件交互直到客户端的JavaScript下载完并且水合完该组件
- 大多数的JavaScript计算还是集中在客户端,而且可能在任何设备上运行(包括配置很低的手机),所以为什么不把计算移到性能更强而且更可控的服务器上?
如图:在Next.js有React服务端组件之前,数据获取需要一层额外的API。
当Web标准慢慢追上了JavaScript框架所推进的技术边界,是时候再来一个大的技术飞跃了。有一个更好的方法来组合更快的应用。
React服务端组件做了什么?
为了解决上面的问题,React团队引入了服务端组件。服务端组件可以各自独立的请求数据而且完全在服务端完成渲染,渲染出来的HTML会流式传输到客户端的React组件树中,适时的穿插进其他的服务端与客户端组件中。
这个过程移除了客户端的重新渲染的必要性,因此了也提升了性能。对于任何客户端组件,既然客户端与服务端是共享计算负载的,水合可以在RSC传输过来时并发进行。
换一种方式来讲就是,服务端,具有强得多的硬件性能并且离你的数据源更近,它们处理计算密集型的渲染任务,然后把需要交互相关的代码传到客户端。
当一个服务端组件需要重新渲染时(state变了),它会在服务端刷新,然后无缝的合并到现有的DOM中而无需刷新页面
。得到的结果就是,客户端的状态被保留了,即使有部分UI是在服务端完成渲染的。
RSCs: 性能与打包大小
RSCs可以帮助减少客户端JavaScript代码的打包体积并提升加载性能。
传统上,当进入应用时,客户端会下载并执行所有代码及其依赖。如果没有一个像React框架那样拥有code-splitting代码拆分能力的情况下,就意味着即使用户在当前页面用不到的代码也会被全部下载。
相反的,RSCs会在服务端解析所有的依赖,也离你的数据源更近。它们只在服务端渲染出代码,这比客户端设备(比如手机)渲染快得多。然后React只把渲染结果加上客户端组件传给浏览器。
换句话说,通过服务端组件,首次页面加载更快了而且更轻量。基础的客户端运行时代码是可缓存的而且大小可控,并且在你应用越来越大时不会增加额外的体积。 用户需要的额外JavaScript代码主要是在你的应用需要更多的客户端交互时才会以客户端组件的形式被加入。
RSCs: 穿插与Suspense的集成
服务端组件与客户端代码是完全穿插在一起的,意味着客户端组件与服务端组件可以在同一个React组件树中渲染。通过把你应用中大多数的代码移到服务端,RSCs可以在服务端快速解析数据依赖,帮助避免客户端瀑布流式的数据请求。
在传统的客户端渲染中,组件在等待异步请求完成时会使用React Suspense来"暂停"它们的渲染进程(并且显示一个兜底状态)。在使用服务端组件时,不管是数据请求还是渲染都在服务端处理,所以Suspense也在服务端管理等待间隙,减少了整体的来回网路请求来提升兜底及整个页面的渲染速度。
需要注意的是客户端组件在首次加载时依然会被服务端渲染(SSR)。服务端组件模型并不是来替换SSR或者Suspense,而是与它们协同工作来为你的应用按需提供用户所需的各个部分。
如图:在Next.js中使用React服务端组件时,数据请求与UI渲染可以在同一个组件中完成。另外,Server Actions(译者注:Next.js在RSC下提供的新能力)提供了让用户能在JavaScript加载之前就能与服务端通信的方式。
RSCs: 局限性
所有服务端组件的代码必须是可串行化的,意味着你不能使用生命周期相关的钩子,如useEffect()
或state。
然而你依然可以在客户端使用Server Actions来与服务端交互,我们后面会讲到。
还有一点就是RSCs不支持持续不断的更新,比如通过WebSockets。在这些场景下,一个客户端的请求或轮询是合理的解决方法。
如何使用React服务端组件?
React服务端组件的魅力之处在于你无需完全真正的了解它是如何工作的以此来充分利用RSC。在Next.js 13.4推出的稳定版App Router
里,它提供了功能最完整的RSCs实现,所有的组件默认都是服务端组件。
如果你需要使用组件生命周期事件例如useEffect()
或state,你需要切换到客户端组件。切换为客户端组件只需要的文件最上面写上"use client"
,如果需要额外的建议,我们建议参考这个文档
在服务端与客户端组件之间取得平衡
需要注意的点是RSCs不是用来替换客户端组件的。一个健康的应用会充分利用这2种组件,RSCs用来做动态数据获取,客户端组件用来处理丰富的可交互性。真正的挑战在于如何选择这2种不同的组件。
作为一个开发者,需要思考在服务端组件及客户端组件之间取得杠杆平衡,利用RSCs来处理服务端渲染及数据获取,与此同时利用客户端组件来获得更好的交互性与用户体验。通过不断调整达到理想的平衡点,你可以创建出高性能,高效率以及引人入胜的应用。
最重要的一点是你需要持续不断地在一些非标准的环境中测试你的应用:模拟性能低的电脑,慢的手机,慢的wifi,然后你可能会被这个能在各种复杂环境中都能良好运行的应用给惊艳到。
RSCs可能不是"终端用户下载太多JavaScript代码"这个问题的终极解决方案,但显然它给了我们选择该何时把计算丢给终端用户设备的能力。
Next.js优化后的数据获取能力
RSCs在服务端获取数据,这不仅仅为数据获取提供了安全保障,也通过减少服务端-客户端的交互来提升了性能。利用Next.js的性能增强,RSCs也提供了智能数据缓存,单个网路请求里的多个数据获取,以及自动化的取消fetch()
请求重复问题,所有这些都为了最大化有效的往客户端传输数据。
最重要的可能是,在服务端获取数据可以有效避免客户端瀑布流式的数据请求,在用户可以交互前,所有的请求一个接着一个,而且很多需要串行发起并解析。服务端数据获取要轻量很多,因为它们不会阻塞整个客户端,并且他们解析的速度更快。
另外,你不再需要Next.js专有的页面维度的方法比如:getServerSideProps()
和 getStaticProps()
,它们无法为单个组件提供足够细粒度的可控性,而且它们常常导致过度的数据获取(当用户打开一个页面,所有的数据都被请求获取了,不管这些数据是不是属于用户正在交互的组件里需要的数据)。
在Next.js App Router中,所有获取的数据默认是静态的,在构建时渲染。当然,我们也很容易修改这样的默认行为:Next.js在fetch
的选项上做了扩展来提供更多缓存与取消缓存规则的灵活性。
你可以使用{next: {revalidate: number}}
选项来在固定的时间间隔后刷新静态数据,或者当后端数据改变时(Incremental Static Regeneration,ISR,渐进式静态生成),与此同时也可以使用{cache: 'no-store'}
选项来获取动态数据(SSR)。
所有这些使得在Next.js App Router中的React服务端组件成为了一个构建高效、安全以及动态获取数据、默认缓存所有数据的应用的有力工具,以此来提供高性能的用户体验。
Server Actions: React踏入数据可变性的第一步
在RSCs的上下文中,Server Actions是你在服务端RSC中定义的方法,它们可以跨越服务端/客户端之间的界限。当用户在客户端与你的页面交互时,他们可以直接调用Server Actions,它们在服务端会被安全的运行。
这种方式提供了无缝的服务端/客户端Remote Procedure Call (RPC)远程调用体验。无需写一个API路由来与服务端交互,你可以直接在客户端组件里调用Server Actions。
同时需要记住的是,Next.js App Router是完全建立在提供智能数据缓存(smart data caching),数据重新验证(revalidating),以及可变性(mutating)之上的。Server Actions in Next.js意味着你可以在同一个发往服务端的请求来回过程中同时修改缓存以及更新React组件树,同时在跳转时维护客户端数据的完整性。
特别的,Server Actions主要被设计用来处理数据库更新或者是表单提交的任务。比如:它们可以渐进式的增强你的表单,也就是说,即使JavaScript还未被加载,用户依然可以与你的表单交互,然后Server Actions会处理表单提交以及表单数据的处理。
Server Actions提供了新的可能性,包括:渐进式增强和移除额外开发API的工作,它们都是为了提供更好的可访问性,易用性,以及开发体验。
让Next.js来处理繁重的事情
Next.js是第一个完整集成React架构的框架,包括React服务端组件,Server Actions,Suspense,Transitions以及所有RSC发布之后的新功能。
当你专注于开发你的产品时,Next.js使用策略性的流式传输及智能缓存来确保你的应用保持渲染时无阻塞,以最高速度服务动态数据。
Next.js努力争取始终提供对最新最尖端的React新功能的支持,与此同时避免牺牲稳定性,可靠性,以及向后兼容性。我们会继续为您的团队提供可靠有效的默认设置来保持快速迭代,并同时为任何级别的项目维护灵活性和可扩展性。
What's Next?
让我们总结下:React服务端组件提供了一个在组件内部与服务端的原生交互方式,让代码更加轻量化及与动态数据交互时的代码加载可认知性。客户端组件还是跟之前一样,功能都还在而且都依然可用。你的新任务就是选择什么时候该用谁。
如果对于这个话题想要更多的指引,可参考Next.js文档。另外,我们有一个App Router playground可以立即开始体验,你可以亲自去体会下它们之间的不同之处 (译者注:应该是App Router+RSC跟之前pages目录的区别)
如果你还对更多的React服务端组件相关的文章感兴趣,以下几篇文章在不同角度都有很好的深入解读及实践: