React SSR架构Streaming Render与Selective Hydration源码分析
Streaming Render
以下 Demo 使用的 react 及 react-dom 版本为 16.14.0
假设我们有如下应用:
import React from 'react'
const Item = () => {
const start = Date.now()
// 人为增加该组件的渲染时间
while (Date.now() - start < 2) {}
return -
List:
{[...new Array(3700)].map((_, i) => {
return
- })}
我们使用 renderToString 来进行服务端渲染:
app.get('/string', async (req, res) => {
const markup = renderToString( )
res.end(`
Demo
${markup}
`)
})通过浏览器访问,可以看到需要等待比较长的时间页面才显示出所有内容
我们换成 renderToNodeStream 再试一试:
app.get('/node_stream', (req, res, next) => {
res.write(`
Demo
`)
const stream = renderToNodeStream( )
stream.pipe(res, {end: false})
stream.on('end', () => {
res.write(`
`)
res.end()
})
})可以看到浏览器中先显示了一部分内容,然后才显示所有内容
这样,当用户访问一个大型的 React 页面时,可以让其尽早地看到一部分内容,从而提供一个比较好的用户体验。
其原理主要是利用了 http 的 Transfer-Encoding: chunked 响应头。
那么,React 每次返回多少内容呢?通过断点调试可以知道,这个值是 16 KB。但是实际返回的长度可能会略大于 16 KB,因为 React 总是会完整的返回标签,比如 ,而不是拆成两部分 和 li>。

为了能够让开发更好的控制流式渲染每次返回的内容,我们可以结合 Suspense,但是由于 16 版本不支持 SSR 使用 Suspense,所以接下来我们换成 v18.2.0 继续我们的实验。
假设我们有如下应用,期待的效果是用户先看到 List:Loading,4 秒后显示 List:a。
import React, {Suspense} from 'react'
const Item = ({index}) => {
return 'a'
}
const Comp = React.lazy(
() =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({default: Item})
}, 4000)
})
)
const App = () => {
return (
-
List:
我们先用 renderToNodeStream 试试,可以看到,页面并没有像我们所期待的那样,而是一开始显示空白,4 秒后才显示 List:a,这实际上已经失去 Streaming Render 的功能了,所以这个函数在 React 18 中被标记为了 deprecated,使用 renderToPipeableStream 来替代。
我们换成 renderToPipealbeStream 就可以看到我们期望的效果了
其原理也很简单,第一次返回的内容为:
-
List:Loading
第二次返回的内容为:
a
其中 这样,React 不用等待整个应用的数据全部准备好才开始返回 HTML 内容给用户了,从而解决了第一个问题:“必须获取到所有数据以后,才能返回内容”。 以下 Demo 使用的 react 及 react-dom 版本为 18.2.0 我们改写一下我们的例子,来看看 React 是怎么解决第二个问题的: 这个例子中 这样,第二个问题:“必须加载到所有 JS 代码后,才能开始进行注水” 也解决了。 为了说明第三个问题,我们先准备一下我们的 Demo: 中的代码主要功能是将 Loading 用 Selection Hydration
import React, {Suspense} from 'react'
const Comp = React.lazy(() => import('./Item')) // Item.js 是一个非常大的文件,大于 600 KB
const App = () => {
return (
)
}
export default AppItem.js 会被单独打包成一个 chunk,我们仍然使用 renderToPipeableStream 来进行渲染,可以看到在 Item.js 这个 chunk 加载完成前,button 已经完成了注水:
// App.js
import React, {Suspense} from 'react'
const Comp = React.lazy(() => import('./ExpensiveComp'))
const Button = React.lazy(() => import('./Button'))
const App = () => {
return (

