You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
在浏览器获得这一整个超过4万行的 html 之前,浏览器是不做任何渲染的,这就代表用户会有很长的白屏时间,于是我们就有了下面的要说的流式渲染。
什么是流式渲染
我们都知道浏览器接受到 html 之后的渲染流程:HTML 解析 -> DOM Tree / cssom tree -> 合成渲染树 -> layout + paint
但比较幸运的是,浏览器不会等解析完完整的 html 文档后,才进行 layout 和 paint。
我们来跑一个简单的 demo 看看
实际上流式渲染的原理并不是很复杂, 为了让效果更直观我们来看看下面这个写了很多 setTimeout 的 demo:
app.get('/ssr-streaming',async(req,res)=>{res.write(` <html lang="en"> <head> </head> <body> `)setTimeout(()=>{res.write(`<div style="background: #111222;width: 100vw;height: 100px;">321</div>`)},100);setTimeout(()=>{res.write(`<div style="background: #123222;width: 100vw;height: 100px;">321</div>`)},500);setTimeout(()=>{res.write(`<div style="background: #333112;width: 100vw;height: 100px;">321</div>`)},1000);setTimeout(()=>{res.write(`<div style="background: #332312;width: 100vw;height: 100px;">321</div>`)},2000);setTimeout(()=>{res.write(`<div style="background: #333432;width: 100vw;height: 100px;">321</div>`)},3000);setTimeout(()=>{res.write(`<div style="background: #335422;width: 100vw;height: 100px;">321</div>`)},4000);setTimeout(()=>{res.write(`<div style="background: #673211;width: 100vw;height: 100px;">321</div>`)},5000);setTimeout(()=>{res.end()},6000);});
可以从浏览器的效果中看到,这些 div 是一段一段被渲染出来的。这归功于浏览器强大的兜底能力,就算我们没有提供闭合的 dom 结构,也能根据已经接收到的数据,进行补全和渲染。基于这一点,我们就可以选择更优的渲染方案,提前我们页面的 TTI 的时间了。
通过 react 的 renderToNodeStream 实现流式渲染(注,renderToNodeStream 是基于 dom 节点切片的,如果都写到一个 dom 里,无论这个节点的字符串多长,都是一次性返回的)
前言
这个问题源于上一次面试拼多多被问到的一套组合拳,性能优化 -> 预渲染 -> 骨架屏 -> ssr -> 流式渲染,前面的几个点因为有所接触问题不大,但流式渲染因为很早已经被 ssr 框架们内置,我居然听都没听过👴🏿❓ 结合最近刚上线的 react 18 ,我们来聊聊流式渲染吧。
接下来我们带着这几个问题来探究一下:
什么是服务端渲染
以 React 的 SSR 为例,React 提供了 renderToString 这个包,用于把 reactDOM 转化成 html 的静态代码。(为了让后面的测试效果明显一点,我写了一个渲染 4 万行 markdown 的 demo)
这样做有什么问题呢?
在浏览器获得这一整个超过4万行的 html 之前,浏览器是不做任何渲染的,这就代表用户会有很长的白屏时间,于是我们就有了下面的要说的流式渲染。
什么是流式渲染
我们都知道浏览器接受到 html 之后的渲染流程:HTML 解析 -> DOM Tree / cssom tree -> 合成渲染树 -> layout + paint

但比较幸运的是,浏览器不会等解析完完整的 html 文档后,才进行 layout 和 paint。
我们来跑一个简单的 demo 看看
实际上流式渲染的原理并不是很复杂, 为了让效果更直观我们来看看下面这个写了很多 setTimeout 的 demo:
可以从浏览器的效果中看到,这些 div 是一段一段被渲染出来的。这归功于浏览器强大的兜底能力,就算我们没有提供闭合的 dom 结构,也能根据已经接收到的数据,进行补全和渲染。基于这一点,我们就可以选择更优的渲染方案,提前我们页面的 TTI 的时间了。

通过 react 的 renderToNodeStream 实现流式渲染(注,renderToNodeStream 是基于 dom 节点切片的,如果都写到一个 dom 里,无论这个节点的字符串多长,都是一次性返回的)
我们可以看到在这个比较极端的 case 里 LCP 的时间明显提前了,这是因为当收到第一段 chunk 之后,浏览器马上进行渲染了。

优化前:
优化后:

流式渲染那么好,那它有什么缺点呢?
lcp 提前了也不完全是好事,因为 react 的 ssr api 是纯文本的,逻辑层会被剥离(dehydrate)。
当我们生成脱水后的 html 之后,需要把逻辑层重新注入代码中,代码才能恢复 event 响应。
这就意味着,在流式渲染中会先看到画面,但页面处于无法响应的阶段,对用户体验会有一定影响。
我们脑补一下,在业务中如果希望提前响应时间我们会怎么做呢?
React 18 的 streaming
在React 18之前,如果应用程序的完整JavaScript代码没有加载进来,hydration就无法启动。对于较大的应用程序,这个过程可能需要一段时间。
但在React 18中,可以让你在子组件加载之前就对应用进行hydration。
通过用包装(warp)组件,你可以告诉React,它们不应该阻止页面的其他部分,甚至是hydration。这意味着你不再需要等待所有的代码加载,以便开始hydration。React可以在加载部分时进行hydration。
这2个Suspense的功能和React 18中引入的其他几个变化极大地加快了初始页面的加载。
The text was updated successfully, but these errors were encountered: