一文带你了解前端渲染模式演进史
The following article is from MoonWebTeam Author MoonWebTeam
本文作者:来自MoonWebTeam的kasswang 腾讯高级前端工程师
本文编辑:kanedongliu
当你准备开发一个新的 Web 项目或进行性能优化时,你是否会纠结究竟用 CSR、SSR 还是其他XXR?本文会带你了解自 Web 诞生以来,Web 前端的渲染模式时如何一步一步演进的,这些渲染模式的设计思路是怎样的、各自有什么优劣指出、分别适用于什么场景,面对这么多种渲染模式我们又应该如何选择。通过阅读本文,你除了能够了解渲染模式的选型外,还能从中获得在性能优化方面的一些灵感和思路。
1. 渲染模式概述
作为前端工程师,大家肯定对 CSR(Client Side Rendering,客户端渲染)和SSR(Server Side Rendering,服务端渲染)这两个名词不陌生,在进行框架选型、性能优化时也会涉及到,他们就是其中的两种渲染模式。
渲染模式简单来说就是 Web 页面内容渲染的方式,比如 CSR 一般指的是从 CDN 获取空壳 HTML 然后在客户端加载并执行 JS 来渲染内容,而 SSR 则是在服务端获取数据后生成 HTML 然后传输给浏览器来渲染内容。在笔者看来,不同渲染模式的实现差异主要表现在:
渲染时机不同:有的渲染模式仅在客户端或服务端渲染,而有的则部分在服务端渲染部分在客户端渲染,还有的会在构建时提前渲染好。 渲染步骤不同:有的一次性渲染完成,有的会分开多次渲染。那为什么会存在那么多不同的渲染模式呢?随着互联网从 Web 1.0 到 Web 2.0、从 PC 到移动互联网的不断发展以及互联网技术的不断进步,互联网的产品形态也随之发展,从一开始的邮件、门户到博客、论坛,再演进到近些年流行的电商、短视频、和线下结合的团购等多种差异化的场景,互联网用户对 Web 页面的交互体验和页面性能有着越来越高的要求,而且对不同业务场景的要求也有差异,因此促使渲染模式自身不断演进来适应业务场景的变化和满足用户越来越高的体验要求。此外,4G和5G的迅猛发展、开发模式和分工的转变、云服务的变革和发展等因素也影响着渲染模式的演变。
下图是笔者整理的渲染模式的演进路线,笔者认为渲染模式的演进主要分为三个阶段:
混沌初开阶段:Web 开始发展的最初10余年,还没有 Web 前端工程师的概念,静态页面和模板技术是当时的主流。 快速发展阶段:随着 Web 产品形态的复杂化,老的渲染模式和开发模式逐渐难以支撑复杂的交互体验,AJAX 的诞生让 CSR 成为主流,Node.js 的快速发展让 SSR 流行起来,CSR、SSR、SSG 成为了主流框架的标配,这三种核心渲染模式也逐渐稳定下来。
探索创新阶段:在这阶段主要是为了能达到极致的性能和更优秀的开发模式,基于 SSR 进行探索性地创新和迭代,很多新出现的渲染模式会和框架结合比较紧密。
2. 渲染模式的演进
2.1. 混沌初开阶段
在 Web 刚起步最开始的 10年,整个 Web 体系仍处于混沌初开阶段,Web 产品形态比较简单,Web 领域还没有细分为前端和后台,此时渲染模式主要分为服务于静态页面的纯静态渲染和服务于动态网页的传统 SSR。
2.1.1. 纯静态渲染
在互联网诞生初期,不像如今面向整个大众群体一样,当时互联网的用户主要是研究机构、高校和政府等相关专业人员,互联网的作用主要是进行简单信息之间的传递,因此当时的产品形态主要是邮件、文章等只读文档(document)。而且当时的网速非常慢,限制了大文件的传输,因此简单即主流,Web 网页也是以文本网页为主,大家对网页的加载速度和交互体验也没有很高的要求——“能打开就行”。
在这样的背景下,Web 开发模式非常简单:编写完 HTML 后,直接将文件上传部署到服务器上。对应的渲染模式也很简单:浏览器向服务端请求 HTML 文档,获取到 HTML 文档后进行渲染,如下图所示。
渲染时机:完整的 HTML 渲染内容在开发机或构建机中提前渲染。
渲染步骤:一次性渲染。
优势:
足够简单:可以不依赖任何前端框架,简单页面的上手成本非常低。 性能最佳:网页资源全部可以部署到 CDN 上。当资源体积不大的情况下,享受极致的首屏性能,首屏可见耗时非常短。 节省资源:通过为资源设置 CDN 缓存,从而减少流量成本。 SEO 友好。
不足:
使用场景局限性比较大,不适用于需要实时的动态数据和数据变化比较频繁的场景。 不适用于交互复杂的场景,如果脱离框架用原生JS 实现复杂逻辑,代码实现和维护成本高。
适用场景:虽然离纯静态渲染流行的时代已经过去近30年了,但目前依旧存在一些适用场景,纯静态渲染模式适用于个人博客、企业主页、学校介绍等不需要动态数据或数据更新频率较低的简单页面。
但随着互联网的快速发展,互联网也逐渐面向大众用户,资讯网站、论坛等产品形态占据主流,而这些都依赖动态数据,纯静态渲染已经不能满足需求了。因此在这一时期动态网页成为主导,涌现出了许多不同的 Web 开发技术,比如 JSP、ASP、PHP 等,这些技术使用的渲染模式本质上就是下面即将提到的传统 SSR 渲染模式。
2.1.2. 传统 SSR
传统 SSR 指的是浏览器每一次向服务端请求 HTML 页面时,服务端都会从数据源获取动态数据,并将数据注入到模板中,最终渲染出 HTML 返回给浏览器,具体流程如下图所示。
渲染时机:在服务端渲染。
渲染步骤:一次性渲染。
优势:
使用局限性较小,支持动态数据。 页面性能较好:在服务端获取动态数据耗时较低的情况下,首屏性能较好。 SEO 友好。
不足:
需要后端模板技术支持,前后端没有分离,增加了开发和维护成本。 需要部署和运维服务器,增加了服务器运营成本。 页面性能可能较差:如果获取动态数据耗时比较高,会导致 TTFB 耗时较高,从而导致白屏时间较长。
适用场景:传统 SSR 是一项比较古老的渲染模式,比较适用于有后端团队、但没有专职的前端开发工程师、以及对页面整体体验要求不高的小团队。由于前后端没有分离,目前不推荐使用该渲染模式。
2.2. 快速发展阶段
随着本世纪初互联网的迅猛发展,Web 产品形态也发生了革命性的变化,页面渲染和交互体验越来越复杂,逐渐对页面的性能和体验有了越来越高的要求,对前端开发工程师的需求随之增加,同时 AJAX 让前后端分离成为了可能。在这个快速发展的阶段,形成了以 CSR 和同构 SSR 为主流、SSG 为辅的渲染模式格局。
2.2.1. CSR
传统 SSR 有个比较大的问题就是前后端没有分离,需要同时掌握前端知识和后端模板技术,在开发和构建部署方面的成本都相对较高。因此在传统 SSR过渡到真正的 CSR 之间存在着一种折衷的 CSR 渲染模式:服务端从数据源获取动态数据后,并不通过模板技术生成完整的 HTML,只是将数据注入到 HTML 的 script 标签中,数据作为全局变量传递给浏览器,而 HTML 只是一个空壳;浏览器接收到携带数据的 HTML 后,通过 JS 利用全局变量中的数据渲染出内容。
笔者团队曾经存在着使用这种折衷的 CSR 渲染模式的祖传 PHP 项目,只要约定好了规范,前端同学确实可以基本不关注 PHP 部分,也基本不需要后台同学介入,实现了某种意义上的前后端分离。但这种方式始终需要利用服务器返回 HTML,有没有方法可以做到真正意义上的前后端分离呢?
AJAX 的出现使真正的前后端分离成为可能。大家都知道,我们可以通过 AJAX 实现在不重新刷新加载页面的前提下,异步调用后端接口获取数据,从而渲染页面内容,这就意味着即便是页面最初始需要的数据,也不需要通过 HTML 返回,可以通过在 JS 中使用 AJAX 异步获取。所以真正的 CSR 渲染流程是这样的(如下图所示):浏览器从 CDN 获取不包含内容和数据的 HTML 后,再从 CDN加载 JS 资源,在 JS 执行阶段通过 AJAX 调用后端接口获取数据,最后利用数据渲染出页面内容。
渲染时机:在客户端渲染。
渲染步骤:取决于代码实现,可以一次性渲染,也可以分步、分模块渲染。
优势:
实现了真正的前后端分离,部署简单,运营成本低。 能通过 SPA 实现复杂且友好的交互体验。
不足:
性能较差:渲染链路较长,涉及到多次串行请求,导致白屏时间很长,首屏性能较差。 SEO 不友好。
适用场景:CSR 仍然是目前最常见、最流行的渲染模式,像 Vue、React 等主流框架默认渲染模式就是 CSR ,适用场景非常广泛。如果对首屏性能要求不高、不考虑极致的首屏性能,以及对资源成本有限制,常规页面建议都采用 CSR 。
虽然 CSR 解决了传统 SSR 没有前后端分离、不支持 SPA 跳转体验的问题,但 CSR 存在首屏性能问题。那么有没有一种渲染模式可以结合这两种模式的优点,既实现前后端分离又实现优秀的跳转体验和首屏性能呢?
2.2.2. 同构 SSR
Node.js 的诞生让同构 SSR 渲染成为现实。同构是指一份代码既能在 Node.js 服务端运行一遍,接着又可以在浏览器再运行一遍。同构 SSR 的具体渲染流程如下图所示:浏览器向 Node.js 服务端请求 HTML 时,服务端执行同构代码,在这个过程中,服务端通过后台接口获取数据后生成 HTML 返回给浏览器,浏览器就可以展示渲染内容了;然后浏览器继续获取 JS 资源后执行同构代码,通过水合(Hydration)过程校验同构代码执行结果是否一致,并将 DOM 元素绑定事件,达到可交互的状态。
渲染时机:在服务端渲染。
渲染步骤:一次性渲染。
优势:
页面性能较好:在服务端获取动态数据耗时较低的情况下,首屏性能较好。 容灾完备:在服务端出现故障后,通常可以降级到 CSR 渲染模式。 统一的心智模型:不需要了解额外的后端模板技术。 SEO 友好。
不足:
页面性能可能较差:和传统 SSR 一样,如果获取动态数据耗时比较高,会导致 TTFB 耗时较高,从而导致白屏时间较长。 需要额外部署和维护 Node.js 服务器,增加了运营资源成本。 写代码时需要考虑代码在服务端的兼容和内存泄漏的规避,对开发人员的技术有一定要求。 由于每次请求服务端同构代码都会执行一遍,当请求量很大时,服务端负载会比较高,对服务器的稳定性和容灾设计是一种考验。
适用场景:同构 SSR 是目前比较流行的渲染模式,Vue、React、Svelte 等主流框架都支持同构 SSR。基于性能考量,笔者团队目前绝大部分新项目都使用该渲染模式。如果对首屏性能要求很高,在 Node.js 服务的开发和稳定性保障方面有一定经验,又能接受额外的服务器成本,同构 SSR是很好的选择。
同构 SSR 存在的性能问题就是白屏时间比较长,针对这方面是否有优化的空间呢?
2.2.3. 流式 SSR
同构 SSR 存在白屏时间较长的问题,问题的根源是同构 SSR 需要等待所有组件依赖的数据接口请求完并渲染完成后才返回最终的 HTML 给到浏览器。其中一个优化的思路就是利用 HTTP 协议的分块传输能力,将完整的 HTML 内容分成多个块依次返回,无需等待所有内容渲染完毕再返回,具体渲染过程如下图所示。
渲染时机:在服务端渲染。
渲染步骤:一次性渲染,分块传输。
优势和不足:流式 SSR 是基于同构 SSR 上的小优化,在页面结构比较复杂、后台接口较多的一些场景下能够显著降低白屏时间,其余的优劣和同构 SSR 一致。此外,优先分块输出 CSS 和 JS 标签可以让静态资源提前加载,可以降低页面的可交互时间。但分块输出的顺序也依赖页面本身结构的顺序,前面的接口请求会阻塞后续的分块输出,因此可能会存在木桶效应。
适用场景:目前 Vue 和 React 都支持流式 SSR 渲染能力,但适用场景比较有限,对于页面结构比较复杂、后台接口较多的页面性能优化效果比较明显,而对于那些结构简单、接口少且耗时短的页面的性能和非流式的同构 SSR 差距不大。
同构 SSR 和流式 SSR 都比较适用于动态页面,对于静态页面或者数据变化频率低的页面是否有更好的解决方案呢?
2.2.4. SSG
除了导致白屏时间较长外,同构 SSR 还有一个问题就是需要额外的服务器资源成本。而 SSG 渲染模式可以同时解决这两个问题。SSG(Static Side Generation)即静态生成,和纯静态渲染模式不同的地方在于 SSG 是利用动态数据进行构建渲染的。SSG本质上是基于同构 SSR 的,同构 SSR是在用户请求的时候实时通过动态数据生成 HTML,而 SSG 则是提前生成。SSG 的渲染过程如下图所示。
渲染时机:在构建服务器中渲染。
渲染步骤:一次性渲染。
优势:
支持动态数据。 实现成本较低:和 CSR、同构 SSR 可使用同一份代码,无需额外工作量。 页面性能较好:可利用 CDN 缓存,无需实时请求后台接口,因此TTFB 耗时很短,白屏时间耗时较短。 SEO 友好。
不足:
使用场景比较受限:不太适用于数据更新比较频繁或数据千人千面的场景。 首屏可见和可交互时间可优化:和 CSR 类似,需要加载 JS 资源后才能进行渲染,首屏可见和可交互耗时较长。 当有成千上万个页面需要批量渲染时,服务负载和渲染时间非常长。
适用场景:和纯静态渲染模式类似,SSG 渲染模式适用于纯静态页面、静态数据较多或数据更新不太频繁的页面,每次数据更新都需要重新构建生成渲染内容。目前主流框架都能同时支持 CSR/SSR/SSG 渲染模式,对于静态数据较多的页面可以结合 SSG 和 CSR 一并使用,针对页面中大部分静态不变的部分使用 SSG,少量动态变化的部分使用 CSR。
从上面的描述可以看到,同构 SSR 的优势在于数据的实时性,短板在于每次都需要重新渲染导致成本增加和性能变差;而 SSG 的优势在于只有数据发生变化才重新构建生成,缓存可以复用,短板在于数据不实时。因此同构 SSR 和 SSG 的优势、不足和适用场景大部分是互补的,从性能、成本和数据实时性的综合考量出发,有没有一种渲染模式能够结合 SSR 和 SSG 的优劣,在低成本下实现高性能呢?下面我们来看下 ISR 是如何实现的。
2.2.5. ISR
笔者团队的一个业务使用了同构 SSR,由于页面不是千人千面的,当有用户首次访问后,在服务端调用后台接口渲染内容,然后缓存起来,设置缓存有效期;下一次用户访问时,如果缓存仍在有效期内,直接返回缓存中的渲染内容;如果失效,重新调用接口渲染内容,重新缓存。这种做法通过牺牲些许数据的实时性来提升页面性能、降低成本。
ISR(Incremental Static Regeneration)即增量静态生成,是 Next.js 首先提出的一种将 SSR 和 SSG 结合起来的渲染模式,主要通过增量渲染的方式来解决 SSG中随着页面的增多导致整体渲染耗时增大的问题。他的思路和上面提到的做法有相似之处:
对比较重要的页面,在构建阶段提前进行 SSG ,并缓存起来;针对不重要的页面,等待有用户访问时才进行渲染(增量静态生成),同时也缓存起来。 用户访问页面时,如果页面缓存没有失效,直接返回缓存内容;如果页面缓存已经失效,重新请求后台接口进行同构 SSR 渲染,将最新渲染内容返回给浏览器,并缓存起来。
渲染时机:在构建服务器渲染(SSG)或运行服务端(SSR)中渲染。
渲染步骤:一次性渲染。
优势:
页面性能较好:大多数情况下不需要请求后台接口和进行渲染流程,TTFB 耗时较短,白屏时间较短。 实现成本较低:Next.js、Nuxt3等框架支持使用同一份代码来进行 CSR/SSR/SSG/ISR 渲染,基本无需额外工作量。 和纯 SSR 相比,后台接口调用量和渲染处理逻辑变少,节省了机器和流量成本。 SEO 友好。
不足:
数据实时性降低:由于引入了缓存,牺牲了数据的实时性。 首屏可交互耗时可优化:需要加载 JS 完成水合过程后才可交互。
适用场景:适用于同时包含较多动态页面和静态页面的场景,对页面首屏性能有一定要求,而且对数据实时性不高。
2.3. 探索创新阶段
经过十多年的快速发展,Web 前端渲染模式逐渐稳定,形成了以 CSR 和同构 SSR 为主导、SSG 为辅的局面。尽管相对稳定,但不存在最完美的渲染模式,各种渲染模式都有其适用场景和局限,开发者们自然希望自己负责的页面能在更低的成本下实现更好的性能和交互体验,因而会不断尝试探索更合适的渲染模式。在当前的探索创新阶段,更聚焦在同构 SSR 和现有框架基础上,基于不同的考量、针对现有渲染模式的不足进行迭代优化,从而产生的新的渲染模式,如 React 的选择性水合和服务端组件、Astro 的孤岛架构等。
2.3.1. ESR
和 CDN 页面相比,SSR 和 ISR 存在一个比较大的性能问题:用户遍布全国各地,渲染服务器一般只部署在三地,大部分用户由于物理距离本身较远导致 TTFB 耗时比请求 CDN 节点要高。而 ESR(Edge Side Rendering,边缘渲染)的出现就是为了解决这个地域带来的性能问题。
ESR 一般的做法是将页面拆成静态和动态两部分,静态部分为 CDN 缓存,动态部分实时获取。用户请求 CDN 资源时,会通过 HTTP 的分块传输能力优先把静态内容流返回给用户,然后再通过专线请求动态 SSR 服务获取动态内容流,返回给用户。渲染流程如下图所示:
渲染时机:静态部分在构建机提前渲染,动态部分在服务端实时渲染。
渲染步骤:分成静态和动态两部分进行渲染。
优势:
页面性能很好:由于直接请求 CDN 资源、走专线通道和分块传输等特性,页面的 TTFB、首屏可见耗时都很低。 SEO 友好。
不足:
整体架构较复杂,改造成本大。 对平台有依赖,依赖边缘计算基础设施建设。 大量的边缘计算节点成本比较高。
适用场景:目前 ESR 在国内还没有大规模用起来,相关工具和基础建设还不成熟,如果是追求页面性能和体验、不考虑成本问题、可以折腾的新业务可以尝试使用。
2.3.2. NSR
上面提到的 ESR 可以看作将 SSR 渲染过程搬到边缘计算节点中进行,同样的,NSR(Native Side Rendering,原生客户端渲染)就可以看作是将 SSR 渲染过程搬到用户的原生客户端中进行,本质上是分布式 SSR。通过分布式 SSR 可以解决两个问题:
可以降低服务端渲染的成本。 在二级页的场景进行预加载可以提升性能,实现页面秒开。NSR 渲染的核心思路是在客户端内置了一个 JS 引擎用于渲染,当用户进入一级页面后,提前进行二级页的渲染资源获取,并交给 JS 引擎进行 NSR 渲染,整个渲染流程如下图所示。
渲染时机:在客户端的 JS 引擎中提前渲染。
渲染步骤:一次性渲染。
优势:
通过分布式 SSR 的方式可以减轻服务端渲染的负担。 通过预加载的方式可以显著提升页面首屏性能,实现秒开。
不足:
非标准化能力,需要客户端额外支持。 往往需要结合预加载使用,命中率低的情况下,会造成非常大的资源浪费。 提前预加载会存在数据实时性问题,需要考虑数据的更新。
适用场景:NSR 一般需要配合预加载使用,适用于对页面性能要求极高、能承担较多资源成本的场景,但需要尽可能地通过人群定向等方式提高预渲染的命中率。
2.3.3. 选择性水合
同构 SSR 虽然比起 CSR 在性能上有一定提升,但依旧存在较多问题导致性能较差:在页面可见前必须调用所有数据接口,在进入水合之前必须加载所有组件的资源,在页面组件可交互之前必须进行完整的水合过程。这些问题的存在会导致最终的页面首屏可见耗时、可交互耗时较长。
而流式 SSR 在一些情况下能够解决 TTFB 长的问题,但如果最开始的接口耗时很长导致阻塞了页面的出流,最终的 TTFB 还是会很长,没有优化效果。那是不是可以先跳过接口耗时长的组件渲染,优先渲染其他组件,后面再渲染这些接口耗时长的组件呢?按照这个思路优化的话,可以尝试调整页面结构的顺序,将接口耗时长的组件放在页面结构的最后面,但这种做法会破坏页面本身的结构,使用场景非常受限。有没有更好的做法呢?
针对这个问题,React 18 提出了选择性水合(Selective Hydration)的解决方案。对于那些不应阻塞渲染出流的非关键组件可以选择使用 Suspense 组件包裹起来,那么这个组件在出流时就会以 Fallback 的内容(比如空白、骨架、loading)渲染,服务端同时进行组件的异步渲染;异步渲染完成后,组件渲染内容和替换代码会立即追加到渲染流中,此时在客户端中组件最终的渲染内容会隐藏,然后将原来 Fallback 的内容替换为最终渲染的内容。此外,通过懒加载方式支持组件的异步加载和水合,可以实现无需等待所有的组件渲染和水合,页面就能交互。选择性水合的渲染过程如下图所示。
渲染时机:服务端渲染。
渲染步骤:分组件独立渲染,分块传输。
优势:
页面性能很好:通过分块传输、分组件异步渲染、加载和水合等方式能有效提升首屏可见和可交互耗时。 使用场景广泛,可快速切换为 SSR/CSR/SSG。 SEO 友好。
不足:
目前只有 React 18 才能支持,其他框架暂无官方支持。
适用场景:可使用的场景比较广泛,追求极致页面性能、使用 React 技术栈的场景建议使用。
2.3.4. 孤岛架构
和选择性水合通过推迟部分组件渲染和 JS 加载的思路不一样,孤岛架构通过消除部分组件 JS 资源的加载和组件水合过程来达到性能优化的目的。我们可以把页面看作汪洋大海,将页面中的纯渲染组件当作是大海的一部分,将可交互组件看作是大海中的一个个孤岛。在实际的渲染过程中,纯渲染组件只会在服务端进行渲染,不需要浏览器加载组件的 JS 资源和进行水合;只有可交互组件才需要在服务端渲染完成后,再到浏览器进行独立的资源加载和水合过程。渲染过程如下图所示。
渲染时机:服务端渲染。
渲染步骤:一次性渲染,分组件独立水合。
优势:
页面性能较好:在可交互组件占比较少的情况下,首屏性能较好,每个组件独立可交互。 SEO 友好。
不足:
使用场景受限:更适合以内容为主体的场景,对于可交互内容较多的情况下,首屏可交互性能和常规 SSR 相比没有太大的提升。
适用场景:使用场景比较受限,适合在非交互部分比重大的以内容为中心的场景中使用。
2.3.5. React Server Components
在页面性能优化方面,React Server Components(RSC,React 服务端组件)的优化思路和孤岛架构有点相似:将页面的组件拆分成服务端组件和客户端组件,服务端组件只会在服务端执行渲染,客户端阶段不会加载服务端组件代码和进行水合;客户端组件则允许在服务端和客户端渲染和水合,跟常规 SSR 的组件表现一致。当服务端组件需要更新数据时,会从服务端获取到更新后的渲染内容然后进行替换更新,也不涉及该组件渲染代码的加载。RSC 可以和选择性水合结合使用,具体的渲染过程如下图所示:
渲染时机:服务端渲染。
渲染步骤:分组件独立渲染,分块传输。
RSC 另外一个重要的理念就是转变前后端分离的思路,将前后端开发的心智模型统一起来,使用 React 同时实现前端和后端的功能,并且通过服务端组件和客户端组件来隔离职责。一般在服务端运行的模块包体会比较大,通过拆分服务端和客户端组件能够让浏览器无需加载服务端组件的代码。
优势:
性能很好:RSC 一般结合选择性水合一起使用,对于服务端组件无需加载组件代码,首屏可见和可交互耗时较低。 对 React 全栈开发友好。 SEO 友好。
不足:
不够成熟和稳定:目前 RSC 仍处于提案阶段,在 Next.js 中仍是实验特性。 对于前后端分离的开发模式来说不是必要的。 React 专属,其他框架不支持。
适用场景:如果希望在 React 全栈开发的场景追求极致的性能可以尝试下,否则使用选择性水合就足够了。由于不够成熟和稳定,总体上暂时不推荐使用。
2.3.6. Qwik
现有的支持同构 SSR 的框架实现的页面到达可交互阶段必须经过以下完整的流程:服务端渲染 HTML 后,客户端加载和解析执行 JS 资源,构建出完整的组件渲染树,然后进行水合,绑定事件后才可交互。加载执行JS 和水合的耗时都比较长,并随着页面的复杂性而线性增长。
Qwik 框架的目标就是需要优化这两部分的耗时,核心思路主要有两点:
框架通过实现组件方法级别的超细粒度的懒加载,让首屏需要加载和解析执行的 JS 资源体积减少到最低。 抛弃复杂的水合过程,在客户端不需要重新执行一次渲染代码,通过更简化的方式实现状态还原,有种从暂停到继续(可恢复性)的感觉。
Qwik 可恢复渲染过程如下图所示:
渲染时机:服务端渲染+客户端渲染。
渲染步骤:首屏先在服务端渲染,然后非首屏在客户端渲染。
优势:
页面性能较好:相较于 SSR 同构渲染,主要优化了首屏可交互耗时。 在框架层面实现了超细粒度力度的 chunk 和懒加载,可以有更灵活的懒加载策略。 资源预拉取可以放到 WebWorker 中进行,在 worker 线程中对 JS 代码进行预解析可以减轻主线程的负担。
不足:
虽然已经发布了正式版,但框架不够成熟,生态不够完善。 如果资源没有预拉取,懒加载可能会导致点击响应有延迟,用户体验较差。
适用场景:理论上适用于结构和交互相对复杂的页面,但由于不够成熟,暂不推荐在生产环境使用。笔者之前也撰写了一篇文章来介绍 Qwik,感兴趣的小伙伴可以前往阅读:LightHouse 跑分 100!这个框架究竟是怎么做到的(一)。
3. 渲染模式的选择
渲染模式能够持续不断演进的根本原因在于大家都希望能以更低的成本获得更好的性能和交互体验,由于存在不同的业务场景导致具有不同的优化侧重点,最终就会导致渲染模式朝着不同的方向演进,在具体的思路和实现上就会有所差异,也就有了各自的优劣和适用场景。
对于渲染模式的选择,一定要结合业务场景来选型,适合业务的渲染模式才是最好的。在选型上笔者会重点考虑以下因素:适用场景、性能和体验、资源成本、开发成本、流行度、成熟度和稳定性以及框架依赖情况。不同渲染模式在多维度的对比情况如下:
渲染模式 | 框架依赖情况 | 性能-TTFB | 性能-FMP | 性能-TTI |
---|---|---|---|---|
纯静态渲染 | 无 | 非常短 | 非常短 | 非常短 |
传统 SSR | 无 | 比较短 | 比较短 | 比较短 |
CSR | 主流框架 | 非常短 | 较长 | 较长 |
同构 SSR | 主流框架 | 一般 | 比较短 | 较长 |
流式 SSR | 主流框架 | 比较短 | 比较短 | 较长 |
SSG | 主流框架 | 非常短 | 比较短 | 比较短 |
ISR | 主流框架 | 比较短 | 比较短 | 较长 |
ESR | 主流框架 | 比较短 | 比较短 | 较长 |
NSR +预渲染 | 主流框架 | 非常短 | 非常短 | 一般 |
选择性水合 | React | 比较短 | 比较短 | 比较短 |
孤岛架构 | Astro 等 | 比较短 | 比较短 | 比较短 |
RSC | React | 比较短 | 比较短 | 比较短 |
Qwik | Qwik | 比较短 | 比较短 | 比较短 |
渲染模式 | 资源成本 | 开发成本 | 流行度 | 成熟度和稳定性 |
---|---|---|---|---|
纯静态渲染 | 非常低 | 一般 | 一般 | 非常高 |
传统 SSR | 一般 | 比较高 | 一般 | 非常高 |
CSR | 比较低 | 非常低 | 非常高 | 非常高 |
同构 SSR | 一般 | 比较低 | 非常高 | 非常高 |
流式 SSR | 一般 | 比较低 | 比较高 | 比较高 |
SSG | 比较低 | 比较低 | 比较高 | 比较高 |
ISR | 一般 | 一般 | 比较低 | 一般 |
ESR | 比较高 | 比较高 | 非常低 | 非常低 |
NSR | 比较高 | 比较高 | 非常低 | 非常低 |
选择性水合 | 一般 | 比较高 | 比较低 | 一般 |
孤岛架构 | 一般 | 比较高 | 非常低 | 比较低 |
RSC | 一般 | 比较高 | 非常低 | 非常低 |
Qwik | 一般 | 比较高 | 非常低 | 非常低 |
以笔者所在的业务为例,业务场景以面向C端用户的移动端动态页面为主,技术栈是 Vue3,对页面首屏性能和页面的稳定性有很高的要求,能够承受一定的资源成本和开发成本,因此当下我们会选择基于 Vue 的流式同构 SSR + ISR 的渲染模式组合,在这个基础上借鉴其他渲染模式的思路,通过首屏拆分、懒加载等方式来优化 TTI。假如我们的技术栈是 React,我们会考虑使用包含流式 SSR + 选择性水合渲染模式的 Next.js 框架来支持业务。
当然,我们不一定需要直接选择成型的渲染模式,不同渲染模式可以结合起来使用。很多情况下我们也可以基于现有的渲染模式下,参考和借鉴其他渲染模式的设计思路进行优化。笔者认为比较有价值的思路有:
流式 SSR 和选择性水合:利用 HTTP 协议分块传输的能力,可以优先输出已就绪模块的 HTML ,对于未就绪模块先输出占位内容,待异步数据加载完成后继续分块输出该模块的 HTML。 ISR:对于数据实时性要求不高的页面,可以缓存起来,同理可以应用到模块、组件缓存中。 孤岛架构和 RSC:对于在浏览器只读的无交互组件,可以考虑无需加载组件的 JS 代码。 Qwik:对于非首屏的组件,可以延迟加载,在空闲的时候才加载组件代码、渲染和绑定事件。
4. 总结
本文首先介绍了前端渲染模式是什么,然后简述了渲染模式演进的原因和路线,之后详细介绍了每一种渲染模式各自希望解决的问题、实现思路、渲染流程、优劣势和适用场景,最后再总结了如何根据业务场景需要选择最合适的渲染模式,也提供了一些从渲染模式中借鉴而来的性能优化思路。希望本文对大家在渲染模式选型和性能优化方面有所帮助。
最后,如果客官觉得文章还不错,👏👏👏欢迎点赞、转发、收藏、关注,这是对小编的最大支持和鼓励,鼓励我们持续产出优质内容。
5. 参考资料
Rendering Patterns(https://www.patterns.dev/posts/rendering-patterns)
Rendering on the Web(https://web.dev/rendering-on-the-web/)
New Suspense SSR Architecture in React 18(https://github.com/reactwg/react-18/discussions/37)
How React server components work: an in-depth guide(https://www.plasmic.app/blog/how-react-server-components-work)
Do you REALLY need SSR?(https://www.youtube.com/watch?v=kUs-fH1k-aM)
现代前端框架的渲染模式(https://juejin.cn/post/7241027834490437669)
边缘渲染是如何提升前端性能的?(https://cloud.tencent.com/developer/article/2142531)
6. 关于我们
MoonWebTeam目前成员均来自于腾讯,我们致力于分享有深度的前端技术,有价值的人生思考。