React Server Components是React团队在2020年底推出的新特性,它允许组件在服务端渲染,只把必要的数据和交互逻辑发送到客户端,大大减少了客户端的JavaScript体积。我们团队在一个项目中尝试了React Server Components,但是刚开始性能并不理想。本文记录了我们从慢到快的完整优化过程,包括遇到的问题、优化的思路、具体的措施和最终的效果。如果你也在尝试React Server Components,希望这篇文章能给你一些参考。

一、背景:为什么尝试React Server Components

先说说我们为什么要尝试React Server Components吧。

我们有一个内容型的网站,主要是文章和商品展示,页面内容比较多,但是交互不算特别复杂。之前用的是传统的客户端渲染,首屏加载的时候需要下载比较大的JavaScript bundle,然后再请求数据渲染页面,首屏时间比较长。

我们试过SSR(服务端渲染),首屏时间确实改善了,但是SSR也有它的问题。比如服务端压力大、 hydration 过程中页面可能闪烁、复杂的缓存策略等。而且SSR只是把HTML先渲染出来,客户端还是需要下载完整的JavaScript bundle来做hydration,bundle体积的问题没有根本解决。

这时候React Server Components出来了。它的核心思想是:把组件分成服务端组件和客户端组件。服务端组件只在服务端运行,不会被打包到客户端的JavaScript中。客户端组件才会被打包发送到浏览器。这样可以大大减少客户端的JavaScript体积。

而且服务端组件可以直接访问数据库、文件系统等服务端资源,不需要通过API来获取数据,减少了网络请求的次数。

听起来很美好,我们决定在一个新项目中尝试一下。

二、初体验:性能不如预期

我们花了一周时间,把项目迁移到了React Server Components架构。迁移完成之后,我们做了性能测试,结果却不如预期。

首屏时间虽然比纯客户端渲染快了一些,但是和传统SSR相比,优势不明显。而且在某些页面上,甚至比SSR还要慢。

我们分析了一下,发现了几个问题:

问题1:服务端组件的渲染时间太长

虽然服务端组件不需要发送到客户端,但是它需要在服务端渲染。如果服务端组件的逻辑很复杂,或者数据查询很慢,那服务端渲染的时间就会很长,首屏时间自然就慢了。

我们有几个页面,服务端组件里做了很复杂的数据查询和处理,导致服务端渲染时间超过了2秒,首屏时间自然就上去了。

问题2:水合(hydration)还是很慢

虽然服务端组件不会被打包到客户端,但是客户端组件还是需要的。我们的项目中,客户端组件的比例还是比较高,导致客户端的JavaScript体积还是不小,hydration时间还是比较长。

而且我们发现,有些组件本来可以做成服务端组件,但是因为用了一些客户端特有的API(比如useState、useEffect),就只能做成客户端组件了。这导致客户端组件的比例偏高。

问题3:数据获取没有优化

服务端组件可以直接访问数据库,这本来是优势,但是我们没有做好数据获取的优化。有些组件重复查询了相同的数据,有些查询没有加索引,导致数据库查询很慢。

而且我们没有用好React Server Components的流式渲染特性。所有数据都查询完了才开始渲染,没有充分利用流式渲染来提前发送部分内容。

问题4:缓存策略不完善

服务端组件的渲染结果是可以缓存的,但是我们刚开始没有做缓存。每次请求都要重新渲染服务端组件,重新查询数据库,服务端的压力很大,响应时间也不稳定。

发现这些问题之后,我们开始了系统性的优化。下面说说我们具体做了哪些优化。

三、优化一:合理划分服务端组件和客户端组件

这是最基础也是最重要的优化。

React Server Components的核心优势就是减少客户端的JavaScript体积,所以我们要尽可能把组件做成服务端组件,只把需要交互的组件做成客户端组件。

我们的做法是:

  1. 从头到尾梳理所有组件,判断每个组件是否需要客户端交互
  2. 不需要交互的组件,全部改成服务端组件
  3. 需要交互的组件,尽量把交互部分抽离出来,做成一个小的客户端组件,包裹在大的服务端组件里面
  4. 尽量把客户端组件放在组件树的叶子节点,减少它的影响范围

比如,我们有一个文章列表组件,整个列表本来是客户端组件,因为每篇文章有一个点赞按钮需要交互。我们把它改成了:列表本身是服务端组件,只把点赞按钮抽出来做成客户端组件。这样客户端只需要打包点赞按钮的代码,不需要打包整个列表的代码。

经过这样的梳理,我们的客户端组件比例从原来的60%降到了20%左右,客户端的JavaScript体积减少了一半以上,hydration时间明显缩短。

这里有一个技巧:尽量把客户端组件做小,只包含必要的交互逻辑。展示逻辑都放在服务端组件里,客户端组件只负责交互部分。这样可以最大限度地减少客户端代码。

四、优化二:优化服务端渲染性能

服务端组件虽然不发送到客户端,但是它的渲染时间直接影响首屏时间。所以服务端渲染的性能也很重要。

我们做了以下优化:

1. 优化数据查询

我们检查了所有服务端组件中的数据查询,给慢查询加了索引,优化了SQL语句。对于一些复杂的查询,我们做了预计算和缓存。

我们还发现,有些组件在循环里做数据库查询,导致N+1查询问题。我们把这些查询改成了批量查询,一次查出所有需要的数据,然后在内存里做关联。

经过这些优化,平均数据库查询时间从原来的几百毫秒降到了几十毫秒。

2. 并行数据获取

原来我们的数据获取是串行的,先查A,再查B,再查C。其实A、B、C之间没有依赖关系,可以并行查询。

我们用Promise.all把没有依赖关系的数据查询并行化,大大减少了数据获取的总时间。比如原来三个查询各需要100毫秒,串行就是300毫秒,并行只需要100毫秒。

3. 减少不必要的计算

我们发现有些服务端组件里做了很多不必要的计算,比如对数据做了多次遍历和转换。我们把这些计算优化了,减少了不必要的遍历,用更高效的算法替代了原来的写法。

4. 用好流式渲染

React Server Components支持流式渲染,可以一边渲染一边发送数据。我们原来的写法是等所有数据都准备好了才开始渲染,这样首字节时间就比较长。

我们改成了流式渲染的写法,把不需要等待数据的部分先渲染发送出去,需要等待数据的部分等数据好了再发送。这样用户可以更快地看到页面的部分内容,感知上的首屏时间更短。

经过这些优化,服务端渲染的平均时间从原来的1.5秒降到了300毫秒左右。

五、优化三:完善缓存策略

缓存是提升性能的利器,我们在多个层级加了缓存。

1. 组件级缓存

对于一些不经常变化的组件,比如页头、页脚、侧边栏等,我们加了组件级缓存。渲染结果缓存起来,下次请求直接用缓存的结果,不需要重新渲染。

我们用的是React Server Components自带的缓存机制,设置合适的缓存时间。对于完全静态的组件,缓存时间设长一些;对于偶尔变化的组件,缓存时间设短一些,或者用失效机制来主动刷新。

2. 数据缓存

对于数据库查询的结果,我们也加了缓存。常用的数据查询结果缓存在Redis里,下次查询直接从Redis取,不需要查数据库。

我们设置了合理的缓存过期时间,并且在数据更新的时候主动清除相关的缓存,保证数据的一致性。

3. HTTP缓存

对于一些完全静态的页面,我们加了HTTP缓存头,让浏览器和CDN可以缓存页面。这样重复访问的时候,直接从缓存取,不需要请求服务端。

经过这些缓存优化,服务端的负载大大降低,平均响应时间也更稳定了。高峰期的响应时间和平时差不多,不会因为并发高而变慢。

六、优化四:客户端性能优化

虽然服务端组件减少了客户端的JavaScript体积,但是客户端组件的性能还是需要优化。

1. 代码分割

我们对客户端组件做了代码分割,用React.lazy和Suspense把不常用的组件按需加载。比如弹窗、抽屉、复杂的表单等,只有在用户需要的时候才加载对应的代码。

这样首屏需要加载的JavaScript就更少了,首屏时间进一步缩短。

2. 优化hydration

我们尽量减少了首屏需要hydration的客户端组件数量。对于一些首屏不需要交互的组件,我们延迟了它们的hydration,等页面空闲了再做。这样首屏的hydration时间就缩短了,页面可以更快地响应用户交互。

3. 减少重渲染

我们检查了客户端组件的重渲染情况,用useMemo、useCallback、React.memo等优化了不必要的重渲染。对于一些复杂的列表,我们用了虚拟滚动,只渲染可视区域的内容,减少了渲染的工作量。

七、优化效果

经过这一系列的优化,我们的性能指标有了明显的提升。

首屏时间(FCP):从原来的2.8秒降到了1.2秒

最大内容绘制(LCP):从原来的3.5秒降到了1.5秒

可交互时间(TTI):从原来的4.2秒降到了1.8秒

客户端JavaScript体积:从原来的320KB降到了120KB

服务端平均响应时间:从原来的1.5秒降到了300毫秒

用户体验也有了明显的改善。页面加载更快了,滚动更流畅了,交互更跟手了。我们的用户反馈中,关于页面慢的投诉明显减少了。

而且服务端的负载也降低了。因为有了缓存,很多请求不需要重新渲染和查数据库,服务端的CPU和数据库压力都小了很多,高峰期也能稳定运行。

八、一些经验和建议

最后总结一些经验和建议,给想要尝试React Server Components的朋友。

1. 合理划分组件是关键

React Server Components的性能优势,很大程度上取决于你能不能合理划分服务端组件和客户端组件。尽量把组件做成服务端组件,只把必要的交互部分做成客户端组件。这是最基础也是最重要的优化。

2. 不要忽视服务端性能

服务端组件虽然不占客户端体积,但是服务端渲染时间直接影响首屏时间。要优化数据查询,并行获取数据,减少不必要的计算,用好流式渲染。

3. 缓存很重要

不管是组件缓存、数据缓存还是HTTP缓存,都能大大提升性能。要根据内容的变化频率,设置合理的缓存策略。

4. 渐进式迁移

如果是已有项目,不要一下子全量迁移到React Server Components。可以先从一些简单的页面开始,积累经验,然后逐步扩大范围。这样风险小,也能在迁移过程中不断优化。

5. 做好性能监控

迁移之后要做好性能监控,关注首屏时间、LCP、TTI、客户端JS体积等指标。通过监控发现性能瓶颈,有针对性地优化。

九、写在最后

React Server Components是一个很有前景的技术方向,它从根本上改变了React应用的架构方式,让我们可以在服务端和客户端之间更灵活地分配计算任务。

但是它不是银弹,不是用了就一定快。想要发挥它的性能优势,还需要合理的组件划分、服务端性能优化、缓存策略等一系列工作。

我们的优化过程不是一蹴而就的,而是在实践中不断发现问题、解决问题,逐步提升的。这个过程虽然辛苦,但是收获也很大。

希望我们的经验能给正在尝试或者想要尝试React Server Components的朋友一些参考。技术在不断发展,新的特性层出不穷,只有不断学习、不断实践,才能跟上技术的步伐。

最后用一句话结束本文:"性能优化没有终点,只有不断的迭代和改进。"愿每一个前端工程师都能打造出流畅、快速的用户体验。