本文主要参考的内容来源是patterns.dev。这个网站收录了许多实用的前端设计模式,大家赶紧收藏起来!
React 发布已经十年了,笔者接触前端差不多也有十年时间了。说实话,如果没有 Head First 系列图书,我可能都没有走上编程这条道路。
Head first
尽管现在看来这系列图书内容可能过时了。
Head First 系列图书让我知道,原来编程也可以这么通俗易懂的,对于刚接触这个领域的同学来说,从这里可以获得很多信心和成就感。 这种风格也一直影响着我,学习和工作、传道授业过程中,我会努力把复杂的事情简化、通俗化,提炼本质。
这十年,前端渲染方式一直在演进,我觉得大概可以分为以下三个阶段:
- 传统 SSR: 那时候前端还没有分离,在 JSP、ASP、Ruby on Rails、Django 这些 MVC 框架下,通过模板来渲染页面。jQuery 是这个阶段的主角
- 前后端分离:从 Node.js 发布,到目前为止,是前端发展最迅速的 10 年。 前后端分离的典型代表是 Angular 和 React、Vue 等框架,我觉得,促进前后端分离的主要原因还是随着需求的复杂化,分工精细化了。 前端可以专注于 UI 的设计和交互逻辑。后端只需要提供 API,不需要关心前端的具体实现。
- 同构前端:这几年前端框架的发展进入的深水区,随着云原生、容器技术、Serverless、边缘计算等底层技术设施的普及,也让‘前端’生存范围延展到服务端。前端开始寻求
UX
和DX
的平衡点
通过这篇文章,你就可以知道近些年前端渲染模式的演变。
废话不多说,直接开始吧。
CSR - 客户端渲染
这个我们再熟悉不过了, 即前端页面在浏览器中渲染,服务端仅仅是静态资源服务器(比如 nginx)。
初始的 HTML 文件只是一个空壳,我们需要等待 JavaScript 包加载和执行完毕,才能进行交互,白屏时间比较长。
- 优点
- 部署简单
- 页面过渡、功能交互友好
- 适合复杂交互型应用程序开发
- 缺点
SEO
不友好- 白屏时间长
- 可能需要复杂的状态管理。时至今日,状态管理方面的轮子还在不停地造
SSR - 服务端渲染
为了解决 SEO 和白屏问题,各大框架开始支持在服务端渲染 HTML 字符串。
SSR 把数据拉取放到了服务端,因为离数据源比较近,数据拉取的速度会快一点。但这也不是完全没有副作用,因为需要在服务端等待数据就绪, TTFB(Time to First Byte)
相比 CSR 会长一点。
SSR 只是给我们准备好了初始的数据和 HTML, 实际上和 CSR 一样,我们还是需要加载完整的客户端程序
,然后在浏览器端重新渲染一遍(更专业的说是 Hydration 水合/注水
),才能让 DOM 有交互能力。
也就说, FCP(First Contentful Paint)
相比 CSR 提前了, 但是 TTI(Time to Interactive)
并没有太多差别。只是用户可以更快地看到内容了。
hydration 的主要目的是挂载事件处理器、触发副作用等等
优点
- SEO 友好
- 用户可以更快看到内容了
缺点
- 部署环境要求。需要 Nodejs 等 JavaScript 服务端运行环境
- 需要包含完整的 JavaScript 客户端渲染程序,
TTI
还有改善空间
SSG - 静态生成
对于完全静态的页面,比如博客,公司主页等等,也可以使用 SSG 静态渲染。
和 SSR 的区别是,SSG 是在构建时
渲染的。
和 CSR 一样,因为是静态的,所以在服务端不需要渲染运行时,部署在静态服务器就行了。
VuePress、VitePress、Gatsby、Docusaurus 这些框架都属于 SSG 的范畴。
优点
- 相比 SSR, 因为不需要服务端运行时、数据拉取,TTFB/FCP 等都会提前。
缺点
- 和 SSR 一样,也有客户端渲染程序、需要进行 Hydrate。
对于
内容为中心
的站点来说,实际上并不需要太多交互,客户端程序还有较大压缩的空间。 - 在构建时渲染,如果内容变更,需要重新构建,比较麻烦
ISG - 增量静态生成
ISG 是 SSG 的升级版。解决 SSG 内容变更繁琐问题。
ISG 依旧会在构建时预渲染页面,但是这里多出了一个服务端运行时
,这个运行时会按照一定的过期/刷新策略(通常会使用 stale-while-revalidate )来重新生成页面。
Progressive Hydration - 渐进水合
上文提到,常规的 SSR 通常需要完整加载客户端程序(上图的 bundle.js),水合之后才能得到可交互页面,这就导致 TTI
会偏晚。
最直接的解决办法就是压缩客户端程序的体积。那么自然会想到使用代码分割
(code splitting)技术。
渐进式水合 (Progressive Hydration )
就是这么来的。
如上图,我们使用代码分割
的方式,将 Foo、Bar 抽取为异步组件
,抽取后主包
的体积下降了,TTI
就可以提前了。
而 Foo、Bar 可以按照一定的策略来按需加载和水合,比如在视口可见时、浏览器空闲时,或者交给 React Concurrent Mode
根据交互的优先级来加载。
React 18 官方支持了渐进式水合(官方叫 Selective Hydration
)。
要深入了解 Progress Hydration, 可以看这个视频。
SSR with streaming - 流式 SSR
这个很好理解。尤其是在最近 ChatGPT
这么火。ChatGPT API 有两种响应模式:普通响应、流式响应
- renderToString → 普通响应。即 SSR 会等待完整的 HTML 渲染完毕后,才给客户端发送第一个字节。
- renderToNodeStream → 流式响应。渲染多少,就发送多少。就像 ChatGPT 聊天消息一样,一个字一个字的蹦,尽管接收完整消息的时间可能差不多,用户体验却相差甚远。
浏览器能够很好地处理 HTML 流,快速地将内容呈现给用户,而不是白屏干等。
下面这张图可以更直观感受两者区别:
来源:https://mxstbr.com/thoughts/streaming-ssr/
对于常规的流式 SSR,优化效果可能没有我们想象的那么明显。因为框架还是得等数据拉取完成之后才能开始渲染。因此,除非是比较复杂、长序列的 HTML 树,至上而下需要较长时间的渲染,否则效果并不明显。
优点
- 相比普通响应,流式响应可以提前 TTFB 和 FCP, 浏览器不用空转等待,可以连续绘制。
缺点
- 数据拉取是 TTFB/FCP 的主要阻塞原因。为了解决这个问题,下文的
Selective Hydration
如何巧妙地解决这个问题。
Selective Hydration - 选择性水合
选择性水合(Progressive Hydration)
是 渐进式水合(Progressive Hydration)
和 流式SSR(SSR with Streaming)
的升级版。主要通过选择性地跳过‘慢组件
’,避免阻塞,来实现更快的 HTML 输出, 从而让流式响应发挥应有的作用。
慢组件
通常指的是:需要异步获取数据、体积较大、或者是计算量比较复杂的组件。
比较典型的慢组件
是异步数据获取的组件, 如下图,未开启 Selective Hydration 的情况,会等待所有异步任务完成后才开始输出,而 Selective Hydration 可以跳过这些组件,等待它们就绪后,继续输出。
我们可以在最新的 Next.js(当前是 13.4) 演示一下。
没有开启 Selective Hydration 的 Demo:
代码语言:javascript复制function delay(time: number) {
return new Promise((resolve) => setTimeout(resolve, time))
}
/**
* 获取关键数据
*/
function getCrucialData() {
return delay(1000).then(() => {
return {
data: Math.random(),
}
})
}
function getData(time: number) {
return delay(time).then(() => {
return {
data: Math.random(),
}
})
}
const Foo = async () => {
const data = await getData(1000)
return <div>foo: {data.data}</div>
}
const Bar = async () => {
const data = await getData(2000)
return <div>bar: {data.data}</div>
}
/**
* 页面