概述
渲染模式分类
- CSR (Client-Side Rendering): 客户端渲染,所有的渲染工作都在浏览器中完成,服务器只提供静态资源。
- SSR (Server-Side Rendering): 服务器端渲染,页面在服务器上生成 HTML,然后发送到客户端。
- SSG (Static Site Generation): 静态站点生成,页面在构建时生成静态 HTML 文件,部署后无需服务器处理请求。
- ISR (Incremental Static Regeneration): 增量静态再生,允许在不重新构建整个站点的情况下更新静态页面。
- SPA (Single Page Application): 单页应用,所有的页面都在一个 HTML 文件中加载,使用 JavaScript 动态更新内容。
- MPA (Multi-Page Application): 多页应用,每个页面都是独立的 HTML 文件,浏览器每次请求都会加载新的页面。
- PWA (Progressive Web App): 渐进式 Web 应用,结合了网页和移动应用的优点,支持离线访问和推送通知。
SSR 的优点
- SEO 友好: SSR 可以生成完整的 HTML 页面,搜索引擎可以更容易地索引内容。
- 首屏加载速度快: 服务器直接返回渲染好的 HTML,用户可以更快地看到页面内容,而不是等待 JavaScript 加载和渲染。
SSR 的缺点
- 服务器负载增加: 每次请求都需要服务器渲染页面,可能导致服务器负载增加。
- 开发复杂性: SSR 需要处理服务器端和客户端的渲染逻辑,增加了开发的复杂性。需要在服务器和客户端之间同步状态,可能导致状态管理变得复杂。
- 输入延迟: SSR需要有一个水合的过程,可能导致输入延迟,因为浏览器需要等待 JavaScript 加载和执行才能使页面可交互。
从SSR到SSG
React/Vue/Angular 等等都从框架层面直接提供了SSR支持,例如在 React 中你可以这样使用:
import React from 'react'
import ReactDOMServer from 'react-dom/server'
const html = ReactDOMServer.render(<h1>Hello, world!</h1>)SSR 最早是为了解决单页应用(SPA)产生的 SEO、首屏渲染时间等问题而诞生的,在服务端直接实时同构渲染用户看到的页面,能最大程度上提高用户的体验,流程类似下面: SSR
但 SSR 引入了另一个问题,既然要做服务端渲染,就必然需要一个实时在线的后台服务(通常是基于 Node.js 的服务)用来承载页面请求,那么:
- 需要服务器的计算资源和公网流量来部署这套服务,并且消耗的资源与页面的访问量成正相关,当页面的访问量突增时,渲染服务也需要进行扩容;
- 服务端只能部署在有限的几个地域,对于距离服务端较远的用户而言,加载速度跟静态资源的 CDN 相比,慢了一个数量级(通常是 1-5ms VS 50-100+ms);
- 日常也存在传统服务端同样的运维、监控告警等方面的负担,团队需要额外的人力来开发和维护。
有没有办法解决这些问题呢?
我们重新对 SSR 进行审视,服务端渲染出的页面,逻辑上讲可以分成下面两大块:
1、变化不频繁,甚至不会变化的内容:例如文章、排行榜、商品信息、推荐列表等等,这些数据非常适合缓存;
2、变化比较频繁,或者千人千面的内容:例如用户头像、Timeline、登录状态、实时评论等。
例如,在一篇文章的页面中,文章的主题内容是偏向于静态的,很少有改动,那么每次用户的页面请求,都通过服务端来渲染就变得非常不值得,因为每次服务端渲染出来大部分内容都是一样的! 我们完全可以将文章的页面渲染为静态页面,至于页面内那些动态的内容(用户头像、评论框等),就通过 HTTP API 的形式进行浏览器端渲染(CSR):
这样做有很多好处:
1、由于文章内容已经被静态化了,所以它是 SEO 友好的,能被搜索引擎轻松爬取;
2、大大减轻了服务端渲染的资源负担,不需要额外做一套 Node.js 服务;
3、用户始终通过 CDN 加载页面核心内容,CDN 的边缘节点有缓存,速度极快;
4、通过 HTTP API + CSR,页面内次要的动态内容也可以被很好地渲染;
5、数据有变化时,重新触发一次网站的异步渲染,然后推送新的内容到 CDN 即可。
6、由于每次都是全站渲染,所以网站的版本可以很好的与 Git 的版本对应上,甚至可以做到原子化发布和回滚。
这便是 Gatsby.js、Next.js 这样的网站生成器解决的问题,他们属于 React/Vue 更上一层的框架(Meta Framework),通过 SSR 把动态化的 Web 应用渲染为多个静态页面,并且对高度动态的内容也保留了 CSR 的能力。
从SSG到ISR/DPR
SSG 这样的模式,看似美好,但存在一个瑕疵:
对于只有几十个页面的个人博客、小型文档站而言,数据有变化时,跑一次全页面渲染的消耗是可以接受的。
但对于百万级、千万级、亿级页面的大型网站而言,一旦有数据改动,要进行一次全部页面的渲染,需要的时间可能是按小时甚至按天计的,这是不可接受的。
为了解决这个问题,各种框架和静态网站托管平台都提供了不同的方案,这里我们介绍 ISR 和 DPR 两种。
ISR
既然全量预渲染整个网站是不现实的,那么我们可以做一个切分:
1、关键性的页面(如网站首页、热点数据等)预渲染为静态页面,缓存至 CDN,保证最佳的访问性能;
2、非关键性的页面(如流量很少的老旧内容)先响应 fallback 内容,然后浏览器渲染(CSR)为实际数据;同时对页面进行异步预渲染,之后缓存至 CDN,提升后续用户访问的性能。
ISR 页面的更新遵循 stale-while-revalidate 的逻辑,即始终返回 CDN 的缓存数据(无论是否过期);如果数据已经过期,那么触发异步的预渲染,异步更新 CDN 的缓存。
这就是增量式更新(ISR)的概念,这个概念最早由 Next.js 在 9.5 版本中提出,下面是一个小 Demo:https://reactions-demo.vercel.app/
在 Next.js 中,你可以使用 getStaticPaths() 来定义哪些路径需要预渲染,通过 getStaticProps() 来获取预渲染需要的数据:
// 定义哪些页面需要预渲染
export async function getStaticPaths() {
return {
// 只有 /posts/1 和 /posts/2 会被预渲染
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
// 其它页面,如 /posts/3,都会返回 fallback 页面,然后 CSR
fallback: true,
}
}
// 定义预渲染需要的数据
export async function getStaticProps({ params }) {
// 拉取对应的文章内容
const res = await fetch(`https://.../posts/${params.id}`)
const post = await res.json()
return {
props: { post },
revalidate: 60 // 数据有效期为 60 秒
}
}但 ISR 存在部分缺陷:
1、对于没有预渲染的页面,用户首次访问将会看到一个 fallback 页面,此时服务端才开始渲染页面,直到渲染完毕。这就导致用户体验上的不一致。
2、对于已经被预渲染的页面,用户直接从 CDN 加载,但这些页面可能是已经过期的,甚至过期很久的,只有在用户刷新一次,第二次访问之后,才能看到新的数据。对于电商这样的场景而言,是不可接受的(比如商品已经卖完了,但用户看到的过期数据上显示还有)。
DPR
为了解决 ISR 的一系列问题,Netlify 在前段时间发起了一个新的提案:
Distributed Persistent Rendering (DPR) https://github.com/jamstack/jamstack.org/discussions/549
DPR 本质上讲,是对 ISR 的模型做了几处改动,并且搭配上 CDN 的能力:
1、去除了 fallback 行为,而是直接用 On-demand Builder(按需构建器)来响应未经过预渲染的页面,然后将结果缓存至 CDN;
2、数据页面过期时,不再响应过期的缓存页面,而是 CDN 回源到 Builder 上,渲染出最新的数据;
3、每次发布新版本时,自动清除 CDN 的缓存数据。
当然 DPR 还在很初期的阶段,就目前的讨论来看,依然有一些问题:
新页面的访问可能会触发 On-demand Builder 同步渲染,导致当次请求的响应时间比较长;
比较难防御 DoS 攻击,因为攻击者可能会大量访问新页面,导致 Builder 被大量并行地运行,这里需要平台方实现 Builder 的归一化和串行运行。
混合渲染
SSR +CSR
SSR 似乎已经解决了 CSR 带来的问题,那是不是 CSR 完全没有用武之地呢?其实并不是。使用 CSR 时,页面切换无需刷新,无需重新请求整个 HTML 的内容。既然如此,可以各取所长,各补其短,于是就有 SSR + CSR 的方案:
- 首次加载页面走 SSR:保证首屏加载速度的同时,并且满足 SEO 的诉求
- 页面切换走 CSR:Next.js 会发起一次网络请求,执行 getServerSideProps 函数,拿到它返回的数据后,进行页面渲染。
二者的有机结合,大大减少后端服务器的压力和成本的同时,也能提高页面切换的速度,进一步提升用户的体验。
SSG + CSR
SSR 需要较高的服务器运维成本。对于某些静态网站或者实时性要求较低的网站来说,是没有必要使用 SSR 的。假如用 SSG 代替 SSR,使用 SSG + CSR 方案,是不是会更好:
- 静态内容走 SSG:对于页面中较为静态的内容,比如导航栏、布局等,可以在编译构建时预先渲染静态 HTML
- 动态内容走 CSR:一般会在 useEffect 中请求接口获取动态数据,然后进行页面重新渲染
虽然从体验来说,动态内容需要页面重新渲染后才能出现,体验上没有 SSR 好,但是避免 SSR 带来的高额服务器成本的同时,也能保证首屏渲染时间不会太长,相比纯 CSR 来说,还是提升了用户体验。
SSR 原理
同构
在服务端渲染中,有两种页面渲染的方式:
- 前端服务器通过请求后端服务器获取数据并组装HTML返回给浏览器,浏览器直接解析HTML后渲染页面
- 浏览器在交互过程中,请求新的数据并动态更新渲染页面
这两种渲染方式有一个不同点就是,一个是在服务端中组装html的,一个是在客户端中组装html的,运行环境是不一样的。‘ 所谓同构,就是让一份代码,既可以在服务端中执行,也可以在客户端中执行,并且执行的效果都是一样的,都是完成这个html的组装,正确的显示页面。也就是说,一份代码,既可以客户端渲染,也可以服务端渲染。
如何做到同构呢?就是将前端的代码打包成一个可以在服务端运行的代码,这样就可以在服务端中执行了。 然后浏览请求到html后,再接管html的渲染和交互。
流程如下
- 用户请求: 用户在浏览器中输入 URL,请求服务器。
- 服务器处理请求: 服务器接收到请求后,解析 URL,确定需要渲染的页面。
- 数据获取: 服务器根据请求的页面,查询数据库或其他数据源,获取所需的数据。
- 页面渲染: 服务器使用获取到的数据,结合模板引擎(如 React、Vue 等)渲染出完整的 HTML 页面。
- 返回 HTML: 服务器将渲染好的 HTML 页面返回给浏览器。
- 浏览器渲染页面: 浏览器接收到 HTML 后,解析并渲染页面,用户可以看到完整的内容。
- 客户端交互: 页面加载完成后,浏览器会执行 JavaScript 代码,使页面具备交互功能。
- 后续请求: 用户在页面上进行交互(如点击链接、提交表单等),浏览器会发送新的请求,服务器重复上述流程。
- 状态同步: 在客户端和服务器之间保持状态同步,确保用户在不同页面间的交互体验一致。