为什么需要服务端组件RSC

Published on
Reading time
18 min read
Authors
本文是对React 18的一些新特性的介绍,主要是为什么需要服务端组件RSC

前言

在react之前流行的asp和php组成的 MPA项目,整体架构基本都是处于在代码中访问数据库,然后通过渲染模板给到客户端,这里有一些问题:

  • 首先,每当用户请求一个新页面时,服务器必须重新从数据库加载数据并完整地重新渲染HTML,这不仅增加了服务器的负载,也使得页面响应时间变长,影响了用户体验。

  • 其次,这种模式下的前后端紧密耦合,使得前端界面的更新和维护变得复杂和困难。此外,随着应用的增长,这种架构模式很难适应灵活的交互需求和动态内容更新,缺乏前端技术如Ajax的灵活性和效率。

  • 最后,多页面应用(MPA)通常不如单页面应用(SPA)在用户交互和视觉连续性上表现良好,容易导致用户界面体验断裂,不利于创建流畅的用户体验。

因此,现代Web开发越来越倾向于使用React等前端框架来构建更为动态和响应迅速的单页面应用。这种方式不仅减轻了服务器的压力,也提升了应用的可维护性和用户体验。

Which framework is better for your project?

传统SSR解决了什么问题,并且带来的系列问题


SSR 关注于初始页面加载,向客户端发送预渲染的 HTML,然后必须用下载的 JavaScript 来hydration,之后它才表现为典型的 React 应用。SSR 也只发生一次.也就是当直接访问到某个页面时,仅靠 SSR,用户可以更快地获取 HTML,但在能够与 JavaScript 交互之前,必须等待一个“全有或全无”的过程

1. 所有数据必须从服务器获取,之前无法显示任何数据。

当你使用 SSR 时,页面在服务器上渲染。这意味着服务器在开始渲染页面之前,需要先获取所有必要的数据。如果你的组件需要从数据库或 API 获取数据,那么服务器必须等待这些数据获取完成后才能渲染页面并发送给浏览器,如果这时候有个接口慢了,这会大大增加页面加载的时间。

想象一下你在一家餐厅点餐。你点了一道需要很长时间准备的菜(比如慢炖牛肉)。服务员(服务器)必须等这道菜完全准备好(数据获取完成)才能把它端上桌子(渲染页面)。因此,你 需要等更长的时间 才能开始吃饭(页面加载时间增加)。

2. 所有JavaScript必须从服务器下载,之前客户端不能用它进行填充。

当浏览器接收到服务器渲染的 HTML 时,需要通过 React 的“水合作用”将这些静态内容变成可交互的内容。为了成功进行水合作用,浏览器中的组件树必须与服务器生成的完全一致。这意味着,所有的组件的 JavaScript 代码必须加载完成后,才能开始水合作用。

假设你收到了一本复杂的拼图书(服务器渲染的 HTML),你需要把每一块拼图(JavaScript 代码)都正确放在对应的位置,才能使这本书变得可以翻页和互动(进行水合作用)。如果有一块拼图不匹配或者缺失,这本书就无法正常使用(水合作用失败)。

3. 所有填充在客户端完成之前,任何东西都不能被交互。

React 进行水合作用时,会一次性遍历整个组件树,直到所有组件都完成水合作用才能结束。这意味着,在水合作用完成之前,你不能与页面上的任何组件进行交互。

就像你在读一本互动故事书(组件树),但在你能点击任何一个互动按钮之前,你必须先读完整本书(完成整个组件树的水合作用)。这会导致你在加载过程中无法进行任何互动,只能等待所有内容加载完毕。

React 18的Suspense怎么解决传统SSR的问题


New Suspense SSR Architecture in React 18 · reactwg/react-18 · Discussion #37
Overview React 18 will include architectural improvements to React server-side rendering (SSR) performance. These improvements are substantial and are the culmination of several years of work. Most...
domain favicongithub.com

为了解决这个问题,React 18 创建了 Suspense ,它允许服务器端 HTML 流式传输和客户端的选择性填充。通过用 <Suspense> 包裹一个组件, 让其他优先级高的组件在不被更重的组件阻塞的情况下加载。

当您在<Suspense>中拥有多个组件时,React 会按照您编写的顺序在树中向下运行,从而使您能够以最佳方式在应用程序中进行流式处理。但是,如果用户试图与某个组件交互,该组件将优先于其他组件。

使用<Suspense>的优点:

1. 服务器上的 HTML 可以通过流式进行传输

通过将页面的一部分,比如主内容区域,包裹在 React Suspense 组件中,我们让 React 不需要等待主部分数据被获取就开始为页面的其余部分流式传输 HTML。React 将发送一个占位符,如loading状态,直到主内容区域的数据准备好。

react suspense

2. 客户端可以进行选择性水合

虽然现在html可以通过流式进行传输了,但是js还没有下载完成,不能水合,所以用户还不能进行交互,就像你等了半天,饭菜都上了,但是你还不能吃,因为你的餐具还没拿到手,还不能开动...

通过选择性水合,我们可以指定优先水合那些用户首先需要交互的关键部分,而不是等待所有的JavaScript文件都加载完毕。这样,页面的重要部分可以更快地变得可交互。

举例说明

假设我们有一个电商网站的首页,其中包括以下几个部分:

  1. 顶部导航栏
  2. 产品列表
  3. 底部推荐栏

在传统的水合过程中,整个页面在所有JavaScript加载完成后才开始水合。选择性水合允许我们首先水合顶部导航栏和产品列表,因为这是用户最先需要交互的部分,而底部推荐栏可以稍后再水合。

jsxEavan.dev
import React, { Suspense, lazy } from 'react'

const NavBar = lazy(() => import('./NavBar'))
const ProductList = lazy(() => import('./ProductList'))
const Recommendations = lazy(() => import('./Recommendations'))

function HomePage() {
  return (
    <div>
      <Suspense fallback={<div>Loading NavBar...</div>}>
        <NavBar />
      </Suspense>
      <Suspense fallback={<div>Loading Product List...</div>}>
        <ProductList />
      </Suspense>
      <Suspense fallback={<div>Loading Recommendations...</div>}>
        <Recommendations />
      </Suspense>
    </div>
  )
}

export default HomePage

这个示例中,我们使用React的Suspense和lazy来实现组件的懒加载。页面会首先加载和渲染NavBar和ProductList,而Recommendations部分会稍后加载和水合。

工作原理

  1. 当用户访问首页时,服务器会返回一个包含初始HTML的页面。
  2. 客户端开始下载主要的JavaScript包。
  3. NavBar和ProductList部分的JavaScript包会首先下载并解析,页面开始这些部分的水合。
  4. Recommendations部分稍后才下载并解析,水合在主要部分完成后进行。

这种方式加快了页面关键部分的水合速度,提高了用户体验,因为用户可以更早地开始与页面交互,而无需等待所有JavaScript包的加载。

单纯ssr中使用suspense的局限性

使用suspend的方式,虽然可以提高用户体验,但是在单纯的ssr中,还是有一些局限性的,比如:

  1. 下载过多的代码:虽然JavaScript代码可以异步传输到浏览器,但用户最终还是需要下载整个网页的代码。随着应用功能的增加,代码量也会变得越来越大。这就好像你去买东西,结果必须买下整个商店的库存,虽然你其实只需要其中几样东西。这让人不禁问:用户真的需要下载这么多不必要的代码吗?

  2. 无意义的客户端处理:现在的做法是所有的React组件在浏览器上都要进行 水合作用,不管它们是否需要交互。这个过程就像在开一个party,明明不是所有的客人都要跳舞,但你却让每个人都穿上了舞鞋。这不仅浪费了资源,还让用户等待更长的时间才能看到和使用网页的内容。问题是,是否真的有必要让所有组件都进 水合作用,即使它们根本不需要在浏览器上与用户互动?

  3. 过多的设备负担:虽然服务器可以轻松处理一些复杂的任务,但大部分的JavaScript代码执行仍然发生在用户的设备上。这有点像让一个小孩子扛起一个大包裹,而实际上强壮的成年人(服务器)本可以更轻松地完成这个任务。这在用户的设备性能较弱时尤为明显,可能会导致网页变慢或卡顿。那么,是否应该把这么多的工作都交给用户的设备来处理呢?

React 为什么需要服务端组件rsc

基于以上,React 创建了 Server Components ( 最重要的是区分开服务端组件和客户端组件,更加细分了颗粒度 )。 RSC 单独获取数据并完全在服务器上渲染,生成的 HTML 会流入客户端 React 组件树,并根据需要与其他服务器和客户端组件交叉使用。

此过程消除了客户端重新渲染的需要,从而提高了性能。对于任何客户端组件,水合作用可以与 RSC 流入同时发生,因为计算负载在客户端和服务器之间共享。

换句话说,服务器功能更加强大,并且在物理上更接近数据源,它处理计算密集型渲染,并仅向客户端发送交互式代码片段。

当 RSC 由于状态更改而需要重新渲染时,它会在服务器上刷新并无缝合并到现有 DOM 中,而无需硬刷新。因此,即使从服务器更新了部分视图,客户端状态也会被保留。

服务端组件rsc的优势

1. 提升了性能(降低Bundle包大小)

在传统的网页应用中,浏览器需要下载并执行整个页面的所有代码和数据依赖项,这往往包括了许多用户当前页面并不需要的部分。如果没有React的代码分割功能,这会导致用户下载大量的无关代码。

而使用RSC(React Server Components),服务器会处理所有的依赖关系,因为服务器通常离数据源更近,处理速度也更快。服务器渲染完这些内容后,只会将处理结果和一些必要的客户端组件发送到浏览器。这意味着,使用RSC的网页加载速度更快,页面更简洁。客户端需要下载的基础代码量也更小,且大小是固定的,不会随着应用的增长而增大。这样,只有在你的应用需要更多的用户交互时,才会额外增加客户端的JavaScript代码。

2. interleaved和Suspense 集成

RSC能够与客户端代码无缝结合,这意味着你可以在同一个React组件树中同时使用客户端和服务器端的组件。通过将大部分代码移到服务器端处理,RSC避免了客户端常见的“瀑布流”式的数据请求问题(即一个数据请求依赖另一个请求完成后才能发起,导致多次延迟)。服务器端可以更快速地获取数据并处理,减少用户等待的时间。

在传统的客户端渲染中,React的Suspense功能允许组件在等待异步数据时“暂停”渲染,并显示一个备用状态。而使用RSC时,数据获取和渲染都在服务器上完成,这样Suspense也可以在服务器端管理等待时间,减少了来回的延迟,使得页面可以更快地渲染并展示给用户。

建议阅读:

  1. https://innei.in/posts/tech/why-react-server-component-1
  2. https://innei.in/posts/tech/why-react-server-component-2

参考文章:

  1. https://www.builder.io/blog/why-react-server-components
  2. https://vercel.com/blog/understanding-react-server-components