一文了解 React 18 SSR
2022/05/03
前言
在项目里使用 SSR 也有一段时间了, 趁着五一小长假来研究一下。
文章就下面几个问题进行解答:
- 什么是 SSR?
- React 18 使 SSR 发生什么变化?
- 如何实现一个简单的 React SSR?
一些概念
SPA
SPA 即单页面应用(Single Page Application)。下面是来自维基百科的解释:
单页应用(英语:single-page application,缩写SPA)是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,而非传统的从服务器重新加载整个新页面。这种方法避免了页面之间切换打断用户体验,使应用程序更像一个桌面应用程序。在单页应用中,所有必要的代码(HTML、JavaScript 和 CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面。
下面的 CSR 和 SSR 都是基于 SPA 来的。
CSR
CSR即客户端渲染(Client-Side Rendering)。
顾名思义,就是在渲染工作在客户端(浏览器)进行。
CSR 过程如下图所示:
图片源自 Shaundai 在 React Conf 2021 的演讲。
下面代码是一个CSR 单页应用的 HTML 结构:
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>Doucment</title>
<script src="//main.js" defer></script>
<link href="//main.css" rel="stylesheet"></link>
</head>
<body>
<div id="root"></div>
</body>
</html><!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>Doucment</title>
<script src="//main.js" defer></script>
<link href="//main.css" rel="stylesheet"></link>
</head>
<body>
<div id="root"></div>
</body>
</html>可以看到,上面 HTML 中只有一个 <div id="root"></div> ,其页面结构需要通过加载的 main.js 来生成,数据也需要通过浏览器发送 Ajax 请求来获取。
这样做可以减轻服务端的压力,但是也会一些问题:
- 首屏渲染慢(渲染前需要下载 JS 和 CSS 等资源),有白屏问题,在 JavaScript 加载时用户会看到一个空白页面,体验差;
- 不利于 SEO,只返回一个基础的 HTML,搜索引擎一般无法获取最终的 HTML。
SSR
SSR 即 Server-Side Rendering(服务端渲染)。
SSR 就是在服务器上获取数据并处理组件来生成 HTML,并将该 HTML 发送给用户。SSR 可以使用户在应用的 JavaScript 包加载和运行之前看到页面的内容。
SSR 的过程如图所示:
React 中的 SSR 有以下几个步骤:
- 在服务器上,获取到整个应用程序的数据。
- 然后,在服务器上,将整个应用程序呈现为 HTML 并在响应中发送。
- 客户端收到数据后,在客户端上加载整个应用程序的 JavaScript 代码。
- 最后,客户端运行 JavaScript 来把事件附加到服务器生成的 HTML 上,以使页面具有交互性(这就是“hydration,以下称为‘注水’”)。
第 2 步中返回的 HTML 结构如下面所示:
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>Doucment</title>
<link href="//main.css" rel="stylesheet"></link>
</head>
<body>
<div id="root">
<h1>Hello, World!</h1>
</div>
<script src="//main.js" defer></script>
</body>
</html><!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>Doucment</title>
<link href="//main.css" rel="stylesheet"></link>
</head>
<body>
<div id="root">
<h1>Hello, World!</h1>
</div>
<script src="//main.js" defer></script>
</body>
</html>虽然 hydration 之前页面的互动性不强(只有<a>标签链接和部分表单可以响应),但是却可以让用户在 JavaScript 仍在加载时看到一些内容,这就解决了页面白屏问题,给用户更好的体验,同时因为在服务端生成完整的 HTML ,所以也解决了 SEO 的问题。
优点:
- 首屏渲染快,白屏时间短,用户体验好;
- 利于 SEO,HTML 在服务端生成,搜索引擎直接抓取到最终页面结果。
缺点:
- HTML 渲染在服务端完成,需要部署并运维服务器,增加成本;
- 需要考虑组件、库的兼容性;
- TTFB(Time To First Byte,浏览器获取到第一个字节的时间)增加。
React 18 中 SSR 的变化
在 React 18 之前的 SSR 是这样的,渲染 HTML 和 Hydration 只有两个选项——“全部”或“没有”。什么意思呢?就是服务端必须获取所有数据才相应并返回 HTML,而当客户端收到 HTML 后必须要加载全部代码来进行注水。这样就有一个问题,就是上面所说的,hydration 之前页面的互动性不强,这对用户体验有一定的影响。
React 18 中关于 SSR 的主要的变化有两点:
- 服务端上,新的流式渲染(Streaming HTML)。从原来的
renderToString方法切换到新的renderToPipeableStream方法即可使用。
- 客户端上,选择性注水(Selective Hydration)。需要从原来的
hydrate切换到hydrateRoot,然后使用<Suspense>来包裹组件。
上述这些功能解决了 React SSR 的三个长期存在的问题:
- 不再需要等待所有数据在服务端加载后再发送HTML。相反,一旦你有足够的数据来显示应用程序的基础部分,就可以发送 HTML,并在 HTML 准备好后流式发送其余的数据;
- 不再需要等待所有的 JavaScript 加载完毕后再开始流化(streaming)。相反,您可以将代码分割与服务器渲染一起来使用。服务端 HTML 将被保留,React 将在相关代码加载时对其进行注水;
- 不再需要等待所有组件水化后开始与页面互动。相反,你可以依靠选择性注水来优先处理用户正在交互的组件,并尽早处理它们。
流式渲染(Streaming HTML)
原来的流式渲染 API renderToNodeStream在React 18 中已经被废弃,可以使用 renderToPipeableStream 来实现流式渲染,该 API 可以使用 Streaming 和 Suspense 的全部特性。
新的流式渲染可以让服务端尽早开始响应并发送 HTML,并且流式渲染的额外内容可以与<script>标签一起配合,把它们放在正确的地方。
来看一下它的用法:
// react-dom/src/server/ReactDOMFizzServerNode.js
// flow 类型定义
type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
nonce?: string,
// 通过 bootstrapScriptConten、bootstrapScripts 和 bootstrapModules 添加客户端入口点
bootstrapScriptContent?: string,
bootstrapScripts?: Array<string>,
bootstrapModules?: Array<string>,
progressiveChunkSize?: number,
// 所有 Suspense Boundary 以上的内容已准备好时触发
onShellReady?: () => void,
// 在 shell 完成前报错时调用
onShellError?: () => void,
// 此事件将在整个页面内容准备好后触发。如果不想使用流,可以用这个代替onShellReady
onAllReady?: () => void,
onError?: (error: mixed) => void,
|};
type Controls = {|
// 取消挂起的 I/O ,切换到客户端渲染模式
abort(): void,
pipe<T: Writable>(destination: T): T,
|};
function renderToPipeableStream(
children: ReactNodeList,
options?: Options,
): Controls// react-dom/src/server/ReactDOMFizzServerNode.js
// flow 类型定义
type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
nonce?: string,
// 通过 bootstrapScriptConten、bootstrapScripts 和 bootstrapModules 添加客户端入口点
bootstrapScriptContent?: string,
bootstrapScripts?: Array<string>,
bootstrapModules?: Array<string>,
progressiveChunkSize?: number,
// 所有 Suspense Boundary 以上的内容已准备好时触发
onShellReady?: () => void,
// 在 shell 完成前报错时调用
onShellError?: () => void,
// 此事件将在整个页面内容准备好后触发。如果不想使用流,可以用这个代替onShellReady
onAllReady?: () => void,
onError?: (error: mixed) => void,
|};
type Controls = {|
// 取消挂起的 I/O ,切换到客户端渲染模式
abort(): void,
pipe<T: Writable>(destination: T): T,
|};
function renderToPipeableStream(
children: ReactNodeList,
options?: Options,
): Controls效果如图所示:
选择性注水(Selective Hydration)
在 React 18 中被 Suspense 包裹的组件的注水过程不再阻止浏览器进行其他工作。React会先对准备就绪的部分进行注水处理,不会被未就绪的部分阻塞。
可以使用 lazy 和 Suspense 来处理那些不需要同步加载的组件:
// App.tsx
import React, { lazy, Suspense } from 'react';
const Component = lazy(() => import('../component'));
// ...
<Suspense fallback={<div>loading...</div>}>
<Component />
</Suepense>
// App.tsx
import React, { lazy, Suspense } from 'react';
const Component = lazy(() => import('../component'));
// ...
<Suspense fallback={<div>loading...</div>}>
<Component />
</Suepense>
选择性注水可以让客户端在其余的 HTML 和 JavaScript 代码被完全下载之前尽早开始对页面进行注水,并且还会根据用户交互优先考虑屏幕中最紧急的部分,创造一种即时注水的错觉。
但是需要注意的是,目前 React 18 中的 SSR <Suspense> 还不支持在请求数据时使用。
实践
具体可看项目代码,这里不再赘述。
项目 Github:https://github.com/axnir/simple-react-ssr
参考文章
Understanding Hydration in React applications(SSR)



