前端渲染方式之SSR渲染

开个坑,记录 SSR 渲染原理以及实践

SSR 介绍

什么是 SSR

指 HTML 在服务端渲染完成,发送到浏览器,由浏览器去绑定事件状态和交互,形成现代前端的完整页面,给浏览器 http 请求直接提供有内容的 html 结果页面
比起 传统 CSR(Client Slider Rendering) 首先要请求 html,请求 js,再次请求数据最终形成 html 页面,SSR 让其页面直出的效果体验很好

在现代的 SSR 一般指的是同构 SSR 的解决方案比如 Nextjs 或者 Nuxtjs 等的,下面我们的讨论以 React 框架中的 server 为代表

为了完成直接渲染的 React 组件转成 HTML 在 node 端,我们要使用到 React 中提供的renderToString API, 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useState } from 'react'
import {renderToString} from 'react-dom/server'


const Counter = () => {
const [count,SetCount] = useState(0)
return (
<div>
<button onClick={()=>SetCount(count+1)}>
点我自增 {count}
</button>
</div>
)
}
export default function App () {
return (
<div>
<h1>Explosion!!</h1>
<Counter />
</div>
)
}

const renderHtml = async (ctx:HttpContext) => {
ctx.success(renderToString(<App/>))
}

当服务端收到请求执行renderHtml方法后,返回如下
<h1>Explosion!!</h1><button>点我自增 <!-- --><!-- --> times</button>
会打上标记
此时客户端接受到 HTML 就即刻开始渲染首屏,但是渲染之后,会存在一个问题是按钮绑定的onClick方法并没有点击响应
因为 SSR 要先进行水合

水合(Hydrate)

在服务端返回的 HTML 类似于一种骨架,用户可以快速的预览,但不具备任何的交互能力,所以需要浏览器去下载 js 把 HTML 转化成有交互性的,一般水合的过程如下

  • 解析和执行 JavaScript: 当浏览器加载并解析 HTML 页面时,遇到页面中的<script>标签,这些标签通常引用打包好的 JavaScript 文件,浏览器会下载并执行这些 JavaScript 文件。这个过程使得 React 组件的逻辑在客户端启动
  • React 组件激活: 在 JavaScript 执行过程中,React 会在客户端重新初始化它的组件。此时,React 通过虚拟 DOM 将当前的页面结构与服务器渲染的 HTML 进行对比,如果保持一致,则直接将事件监听器与其他的交互逻辑绑定在已有的 DOM 上,即水合的核心
  • 页面可交互:

实践例子

项目背景介绍

比如在 MIX CSR 的场景下,一个丰富的电商平台或者是个人博客之类的,下面假设为一个电商的移动端,如果页面存在复杂的组件,耗时会很长,解析并执行 bundle js 的时间最长,阻塞了首屏渲染

渲染流程分析

一个商品类页面的加载可以拆解成服务端加载耗时和客户端加载耗时

  • server 是一个聚合的接口,去获取页面组件结构和组件数据的职责,整体耗时取决于最慢的服务

用 SSR 改造之后,在组件对应服务器执行完成后就开始渲染,而不是等待

优化原理

在 React18 中为 SSR 提供了两个重要的支持:RSC(React Server Component)流式渲染

流式渲染

服务端将 HTML 分段传输给浏览器,不需要等待服务器生成整个页面后在返回给浏览器,这样可以更快的启动 HTML 渲染可以有效的提高 FCP、FMP 等性能指标

流式渲染早在 React16 就已经被支持,但是只能是自上而下的 DOM 渲染,不能对组件渲染,不发控制优先级和细腻度

在 React18 中提出了 Suspense + Data fetching 的组合,可以更加细腻度的决定渲染
如下

1
2
3
4
5
6
7
8
9
10
11
12
13
import { fetchData } from '@/api'
const Component = async ({loading?:boolean})=>{
const data = fetchData()
return loading ? <div>loading</div> : <div>{data}</div>
}

const show = ()=>{
return (
<Suspense fallback={<Component loading = {true} />}>
<Component />
</Suspense>
)
}

过程:

  • fetchData() 可以在服务端执行
  • 服务端请求完成前,返回fallback的组件,流式推送给客户端
  • 完成后,Suspense 会携带数据,其 html 片段被推送给客户端替换之前的 fallback

流式渲染可以使组件独立在服务端发起请求并在取得结果后立刻渲染在页面上,而不受其他组件干扰

RSC

RSC 是 react 服务端渲染组件,在整个 SSR 的渲染流程中,仅在服务端运行一次,生成一段类 HTML 脚本返回给客户端,针对这个组件做了renderToString,所以没有浏览器的方法也没有状态等的
不需要水合、在客户端也不需要加载或执行任何的 JS 资源

1
2
3
4
5
6
7
8
9
10
import db from 'store'
async function HomePage () {
const link = db.connect('localhost','root','password')
const data = await db.query(link,'SELECT * FROM products',)
return (
<div>
{data}
</div>
)
}
  • 在服务端执行,拥有 nodejs 环境
  • 只执行一次,副作用直接在 render function 中去执行,可以使用 async

RSC 的 AST 最后会和其他普通的 React 组件共同渲染出整个页面

实际应用

在一些复杂场景下,可以将页面与组件的数据请求工作包装成 RSC,并可以使用更快的 RPC 去 fetch 数据
这样做的好处有

  • 减少页面 JS 资源体积,优化水合速度,将耗时的渲染工作放在服务端完成,降低水合过程中对低端设备的依赖
  • 请求数据变得更快