Skip to content

极致的网页加载速度——SSR技术调研

最近在看第19届D2论坛的分享,看到了淘宝团队分享的通过端边云协同SSR技术将页面的加载速度提升到了非常可怕的地步(100ms首帧、200ms满屏),所以决定进行一下SSR技术的调研,看看现在的SSR技术到底发展到了什么程度。

现有网页渲染架构

首先先根据发展历程,捋一遍网页渲染架构的几种类型。

纯静态网页

纯静态网页

纯静态网页在打包阶段就渲染完成,图片来源:Next.js Visually Explained: Partial Pre-rendering (PPR)

这里的纯静态网页包括直接编写HTML文件,以及使用模板引擎生成HTML文件。

用户访问一个网页,服务器返回一个固定的HTML文件,浏览器解析HTML文件并显示出来。

这种形式的好处是:

  • 加载速度快,因为静态内容生成后就直接使用CDN缓存,用户访问时直接从CDN获取,传输链路短
  • SEO友好,因为搜索引擎可以很方便的抓取静态内容,并且静态内容更容易被搜索引擎收录。
  • 几乎没有服务器压力,因为服务器基本只需要进行一次静态内容的生成,后续用户访问时都是从CDN获取。

但是这种形式也有缺点:

  • 无法进行个性化,因为所有网页的内容都是固定的,无法根据用户信息生成不同的内容,基本只能用于新闻、博客等场景;
  • 切换页面时会出现白屏,且加载时间较长,因为每次切换页面都需要重新加载HTML文件。

传统SSR

传统SSR

传统SSR只在请求时进行渲染,图片来源:Next.js Visually Explained: Partial Pre-rendering (PPR)

在用户进行访问时,服务器根据用户信息,使用模板引擎渲染好完整的HTML文件,然后返回给用户。

同时也可以附加js,在客户端中的通过js中执行复杂逻辑,比如接口请求、数据处理等。

这种形式的好处是:

  • 可以进行个性化,因为服务器可以根据用户信息生成不同的内容;
  • 相比于客户端渲染,可以将接口相关的逻辑都在服务端进行,减少接口响应时间对页面渲染的阻塞。

而缺点也同样很明显:

  • 服务端一般需要执行获取数据的逻辑后才能返回HTML文件,可能出现这些逻辑阻塞HTML文件返回,导致长时间白屏
  • 服务端渲染压力大,因为默认情况下需要根据每个请求都进行动态渲染;
  • 切换页面时要么按照静态页面的思路,重新加载HTML文件,要么按照客户端渲染的思路,完全使用js动态生成页面,不够灵活;
  • 后端渲染逻辑和前端逻辑耦合,不利于编写复杂的前端逻辑。

客户端渲染(Client Side Rendering,以下简称CSR)

顾名思义,就是将渲染逻辑都放在客户端进行。

用户访问一个网页,这时一般通过CDN返回一个基本是空白的HTML文件,相关的页面渲染逻辑都是通过js来执行的。

这种形式的好处是:

  • 渲染逻辑完全由前端控制,依托于react、vue等框架可以非常方便地编写复杂的前端逻辑,符合前后端分离的开发模式;
  • 页面切换时不会出现白屏,因为不需要重新加载HTML文件,只需要加载和执行对应的js文件。

但是这种形式也有缺点:

  • 页面加载时间较长,需要等待js加载完成,才能进行页面渲染,特别是如果涉及到接口请求,还需要等待接口返回数据后才能进行页面渲染;
  • SEO极差,因为返回的HTML基本是空白的,搜索引擎无法抓取到具体页面内容。

同构SSR

即以NextJS和NuxtJS为代表的,在服务端和客户端使用同一套架构和代码的SSR技术。

利用react、vue等框架提供的服务端渲染能力,在服务端进行组件的渲染,初次请求时能够返回完整的HTML文件给客户端,而后续的页面切换则变为完全的CSR,不需要重新加载HTML文件。

这种形式的好处是:

  • 首屏加载时间短,因为初次请求时能够返回完整的HTML文件;
  • 页面切换时不会出现白屏,因为不需要重新加载HTML文件,只需要加载和执行对应的js文件;
  • 对前端门槛较低,因为依托于react、vue等前端框架,大部分逻辑可以做到服务器和客户端的复用;
  • SEO友好,因为初次请求时能够返回完整的HTML文件,搜索引擎可以抓取到具体页面内容。

同样的,这种形式的缺点如下:

  • 服务端渲染压力大,和传统SSR一样,需要根据每个请求都进行动态渲染;
  • 深度优化并不轻松,虽然使用的门槛不高,但是如果要进行深度优化,依然需要对服务端渲染的逻辑有较深的理解,需要进行额外学习

端边云协同SSR

而淘宝这次分享的端边云协同SSR,则是在同构SSR的基础上,利用react的流式渲染能力,再搭配边缘节点的计算能力,以及客户端的缓存策略,实现了客户端、边缘节点和云端的三端协同渲染。

流式SSR

首先是流式SSR,利用react的流式渲染能力,可以实现根据请求先返回部分同步渲染完成的内容,而其他需要等待的内容则通过流式渲染的方式慢慢返回,最终完成整个页面的渲染。

注意,这里说的渲染是全部在服务端进行的,而不是常见的客户端进行接口请求,然后根据响应内容动态进行渲染。

可以访问我制作的这个demo,感受一下流式SSR的效果:流式SSR Demo

demo的页面结构

首先说明一下页面结构,主体是三个卡片,每个卡片的渲染逻辑不同:

  1. 用户信息卡片:获取用户信息后渲染展示;
  2. 会员折扣卡片:获取用户信息后,如果不是会员,则获取折扣信息,然后渲染展示;
  3. 会员权益卡片:获取用户信息后,如果是会员,则获取权益信息,然后渲染展示。

demo的url末尾是会员id,将其修改为2即可看到会员权益卡片。

流式SSR的性能面板

使用性能面板进行分析,可以看到一件非常有趣的事情——在大约1.5秒时,就完成了首屏渲染,但是页面请求并没有结束,而是在用户信息卡片和会员折扣卡片陆续出现后,才正式完成请求。

也就是说,服务端在完成初次渲染返回后,会继续进行后续内容的渲染,并且渲染完成多少就返回多少,而不是等到所有内容都渲染完成后再返回。

这个demo同时也有模拟传统SSR的版本CSR的版本,可以对比感受一下。

传统SSR的性能面板

CSR的性能面板

三者的用户访问效果对比如下:

流式SSRCSR传统SSR
首屏加载时间0.98s1.20s4.57s
用户信息卡片加载时间1.39s4.44s4.57s
会员折扣卡片加载时间2.39s5.85s4.57s

造成这样的原因也很好理解,获取用户信息和后续获取折扣信息的逻辑都比较耗时,而传统SSR需要等待所有内容都渲染完成后再返回,所以首屏加载时间会非常长。

而CSR同样因为需要和服务器通信两次获取这些信息然后再进行渲染,导致两个卡片的加载时间都非常长。

流式SSR的缺点

虽然流式SSR在首屏加载时间上表现非常优秀,但我在实践过程中也发现了一些问题。

首要就是有一定的学习成本,虽然和CSR都是基于react框架,但是nextjs还是没做到双端同构,需要额外区分服务端组件,很多客户端组件的逻辑无法直接复用,甚至需要调整开发思路

比如demo中的用户信息卡片和会员折扣卡片,按照传统CSR的思路,既可以分别写成两个子组件,然后交由父组件进行渲染逻辑的组织,也可以都写成一个组件,在一个组件内通过state控制各部分的渲染逻辑。

但是在如果要使用流式SSR,就需要这两个延迟渲染的组件都写成服务器组件,并且会员折扣卡片组件必须写成会员信息卡片的子组件,这样才能保证先获取用户信息,再判断是否渲染会员折扣卡片。

端边云协同SSR

流式SSR可以说是大幅提升了首屏渲染的时间,但是每次请求都进行服务端渲染无疑是抛弃了CDN进行缓存的优势。所以在此基础上,还需要将纯服务端渲染和边缘节点结合起来,缩短客户端获取内容的网络传输链路。

这里先贴一张PPT中的架构图,由于我没有在现场亲自聆听,只能结合我的个人认识进行一些解读,如有错误,欢迎指正。

端云同构能力

淘宝的端云同构能力

这一套架构中,首先要解决的问题就是如何保证客户端组件和服务端的组件能够使用同一套习惯进行编写。

WebView和Node环境共通的API自不必说,但是还有大量API是浏览器独有的,比如windownavigator等,还App提供的一些API,更是如此。

这里淘宝的做法是,所有的这些API提供的信息封装入JS Bundle中,然后在请求时,将JS Bundle发送给服务器,从而实现服务端组件能和客户端组件一样获取这些信息。

端边云协同渲染及缓存

先是最容易想到的利用客户端、边缘节点进行缓存,从而缩短静态资源的传输链路。

首先,如果是纯客户端组件和静态组件自不必说,其不需要根据请求动态生成,可以直接使用CDN缓存。

而服务端组件,其实同样也可以如此。以Nextjs为例,如果使用预渲染,在打包时,该框架会根据组件所执行的逻辑,将其划分为动态组件静态组件,然后分别进行打包:

NextJS的预渲染和利用边缘节点进行缓存,图片来源:Next.js Visually Explained: Partial Pre-rendering (PPR)

结合配图,可以看到,在打包时(Build time),一部分的静态组件就被分发到了边缘节点(Edge)中,当用户请求资源时,会直接从边缘节点获取完全静态的部分,同时,边缘节点会请求服务端(Server),让其进行动态组件的渲染,并通过流的形式返回给客户端。

淘宝的端边云协同SSR

而淘宝的这套架构中,不仅利用了边缘节点进行缓存,还利用了客户端的缓存,设置利用了边缘节点的计算能力,让部分不依赖用户信息的动态组件在边缘节点进行预渲染,并且缓存在边缘节点中。

这样做,一方面缩短了同一用户二次访问时请求资源的时间,另一方面,其他用户访问时,对于之前在边缘节点进行了缓存的内容,也能更快地访问。

⾃适应渲染

淘宝的⾃适应渲染

除了需要根据用户请求时的相关信息进行动态渲染,还有一种场景则是根据用户的设备状态(包括网络状态、设备性能等)进行渲染。

如果是传统的CSR,则需要将所有的组件逻辑都下载到客户端中,然后再进行渲染,这就导致了部分逻辑代码的不必要传输。

而通过端云同构能力,则可以在边缘节点或服务端进行用户设备状态的判断,从而进行不同的渲染,直接返回符合当前用户设备状态的页面。

这样做,一方面加快了页面加载速度,另一方面,也减少了客户端的负担,避免了不必要的资源传输。

WebView缓存池流程设计

淘宝的WebView缓存池流程设计

除了缩短传输的时间和减少传输资源的体积,还可以考虑进行客户端的预加载和缓存。

由于是通过淘宝App进行访问的,所以淘宝对内部的WebView进行了定制,设计了一套WebView缓存池机制。

如图中所示,在App启动后,会创建多个WebView请求资源进行预加载,并进入到缓存池中。当用户打开页面时,则可以直接打开缓存池中的WebView,从而加快页面加载速度。

总结

淘宝的这套端边云协同SSR技术,主要是利用了react框架的流式渲染能力,再通过三端缓存、预加载、自适应渲染等技术,实现了页面加载速度的极致优化。

不过个人暂未完全按照这套架构进行实践,可能存在理解不够透彻的地方,也欢迎大家指正。