为什么需要服务端组件RSC

Published on
Reading time
13 min read
Likes
Authors
本文将介绍React 18的一些新特性,重点讨论为什么需要服务端组件(RSC)

前言

在React出现之前,传统的Web开发通常使用ASP和PHP构建 多页面应用(MPA)。这种架构通过在服务器端访问数据库,渲染模板,然后将生成的HTML发送给客户端。然而,这种方式存在一些问题:

  • 服务器负载高,响应时间长:每次用户请求新页面时,服务器都必须重新加载数据并完全重新渲染HTML,这增加了服务器的负担,延长了页面的响应时间,影响用户体验。

  • 前后端耦合紧密,维护困难:这种模式下,前界面的更新和维护变得复杂,因为前后端紧密耦合。随着应用的增长,这种架构难以满足灵活的交互需求和动态内容更新。

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

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

Which framework is better for your project?

传统SSR解决了什么问题,以及带来的问题


服务器端渲染(SSR)关注于初始页面加载,它向客户端发送预渲染的HTML,然后通过下载的JavaScript进行水合(hydration),使页面变为可交互的React应用。传统的SSR解决了一些SEO和初始加载速度的问题,但也引入了新的挑战:


sequenceDiagram
  participant 用户 as User
  participant 浏览器 as Browser
  participant 服务器 as Server
  participant 数据库 as Database

            用户->>浏览器: 输入网址
            浏览器->>服务器: HTTP 请求
            Note over 服务器: 解析请求
            服务器->>数据库: 查询数据
            Note over 数据库: 处理查询
            数据库->>服务器: 返回数据
            Note over 服务器: 渲染 HTML
            服务器->>浏览器: 返回完整的 HTML 页面
            浏览器->>用户: 显示页面

传统SSR流程图

1. 所有数据必须从服务器获取,页面才能显示任何内容。

使用传统SSR时,服务器需要在开始渲染之前获取所有必要的数据。如果某个数据源响应缓慢,整个页面的渲染都会被阻塞,导致加载时间增加。

想象你在餐厅点了一道需要长时间准备的菜(如慢炖牛肉)。服务员(服务器)必须等菜完全准备好(数据获取完成)才能上菜(渲染页面)。因此,你需要等待更长时间才能开始用餐(页面加载)。

2. 所有JavaScript必须先下载,才能进行水合。

浏览器接收到渲染的HTML后,需要下载所有的JavaScript代码,才能将静态内容变为可交互的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
preview image

为了解决上述问题,React 18引入了Suspense,它允许服务器端HTML流式传输,并在客户端进行选择性水合。通过使用<Suspense>包裹组件,可以让优先级高的组件先渲染,不被较慢的组件阻塞加载。


sequenceDiagram
  participant User as 用户
  participant Server as 服务器
  participant React as React
  participant Comments as 评论组件

    User->>Server: 请求页面
    Server->>React: 生成初始 HTML
    React->>Server: 返回非交互内容的 HTML
    Server->>User: 响应请求,发送 HTML
    User->>React: 接收并渲染 HTML
    React->>React: 初始化非交互内容
    React->>Comments: 请求加载 Comments 组件代码
    Comments->>Server: 加载 Comments 组件代码
    Server->>Comments: 返回 Comments 组件代码
    Comments->>React: 提供 Comments 组件
    React->>React: 在 Suspense 中水合 Comments 组件
    React->>User: 允许与页面交互

Suspense工作流程

当在<Suspense>中包含多个组件时,React会按照组件在树中的顺序进行渲染,实现流式处理。如果用户尝试与某个组件交互,该组件的加载和水合将优先进行。

使用<Suspense>的优点

1. 服务器端HTML可以流式传输

通过将页面的某些部分(如主要内容区域)包裹在<Suspense>中,React无需等待所有数据获取完毕就可以开始为页面的其他部分流式传输HTML。React会先发送占位符(如加载状态),直到主要内容的数据准备好。

React Suspense

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

虽然HTML可以流式传输,但JavaScript可能还未完全下载,导致用户暂时无法交互。通过选择性水合,可以优先水合用户首先需要交互的关键部分,而无需等待所有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的Suspenselazy实现组件的懒加载。页面会优先加载和渲染NavBarProductListRecommendations部分会稍后加载和水合。

工作原理

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

这种方式加快了页面关键部分的水合速度,提高了用户体验,因为用户可以更早地开始与页面交互。

使用Suspense的局限性

虽然Suspense提高了用户体验,但在纯SSR中仍存在一些局限:

  1. 下载过多的代码:尽管JavaScript代码可以异步加载,但用户最终仍需下载整个应用的代码。随着应用功能的增加,代码量也会增大,导致用户下载大量可能不必要的代码。

  2. 无意义的客户端处理:所有的React组件,无论是否需要交互,都需要在浏览器中进行水合。这增加了客户端的负担,延长了页面可交互的时间。

  3. 设备性能负担:大部分JavaScript代码的执行都发生在用户的设备上,对于性能较弱的设备,可能导致网页变慢或卡顿。

React需要服务端组件(RSC)

基于以上问题,React引入了服务端组件(RSC),它进一步区分了服务端组件和客户端组件,细化了组件的颗粒度。RSC可以单独获取数据,并完全在服务器上渲染,生成的HTML会流入客户端的React组件树,根据需要与其他服务端和客户端组件交叉使用。

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

换句话说,服务器更强大,且更接近数据源,它处理计算密集的渲染,只向客户端发送必要的交互代码片段。当RSC因状态更改需要重新渲染时,它会在服务器上刷新,并无缝合并到现有的DOM中,客户端状态得以保留。

服务端组件(RSC)的优势

1. 提升性能,降低Bundle大小

在传统的Web应用中,浏览器需要下载并执行整个页面的所有代码和数据依赖,可能包括用户当前页面不需要的部分。而使用RSC,服务器处理所有的依赖关系,只将结果和必要的客户端组件发送到浏览器。这意味着页面加载速度更快,客户端需要下载的JavaScript代码更少,且大小是固定的,不会随着应用的增长而增大。

2. 与Suspense的无缝集成

RSC能够与客户端代码无缝结合,可以在同一个React组件树中同时使用客户端和服务端组件。通过将大部分代码移到服务器端处理,RSC避免了客户端常见的数据请求“瀑布流”问题。服务器端可以更快速地获取并处理数据,减少等待时间。

在RSC中,数据获取和渲染都在服务器上完成,Suspense也可以在服务器端管理等待,减少了来回的延迟,使页面更快地渲染并展示给用户。

参考链接

感谢以下资源的帮助和启发:

1.What are React Server Components? Understanding the Future of React Apps faviconWhat are React Server Components? Understanding the Future of React Apps
2.Understanding React Server Components - Vercel faviconUnderstanding React Server Components - Vercel
3.为什么是 RSC (一) | 静かな森 favicon为什么是 RSC (一) | 静かな森
4.为什么是 RSC (二) | 静かな森 favicon为什么是 RSC (二) | 静かな森
5.Routing: Loading UI and Streaming | Next.js faviconRouting: Loading UI and Streaming | Next.js