当前几种常见的前端性能优化方案仍然不可避免地会存在一些缺点。本文在 ESI (Edge Side Include) 的基础上,提出了一种新的优化思路:边缘流式渲染方案(ESR),即借助 CDN 的边缘计算能力,将静态内容与动态内容以流式的方式,先后返回给用户。
背景
对于 web 页面来说,首跳场景(例如 SEO、付费引流)的性能普遍比二跳场景下要差。原因有多种,主要是首跳用户在连接复用,和本地资源缓存利用方面,有很大的劣势。首跳场景下,很多在端上的优化手段(预加载,预执行,预渲染等)无法实施。
在客户端缓存能力无法利用的情况下,利用 cdn 距离用户近的特性,可以结合缓存做一些性能优化。
思路
思路 1:SSR
为了性能优化考虑,我们一般都会通过服务端渲染(SSR) ,将首屏动态内容直接服务端输出。
这种方式的优点是一次 html 返回即可包含页面主体内容,不需要浏览器二次请求接口后再用 js 渲染。但这种方式的缺点也比较明显,对于距离服务端远,或者服务端处理时间较长的场景,用户会看到较长时间的白屏。而且即使 html 返回完成了,用户并不会立即看到内容,页面还需要加载前置的 js,css 等资源后,才能看到内容。
思路 2:CSR + CDN
为了减少白屏时间,考虑利用 CDN 的边缘缓存能力,可以把页面 html 直接缓存在 cdn 节点上。但对于大部分场景来说,页面的主体内容都是动态,或者个性化的,把全部 html 内容缓存在 cdn 上对于业务影响较大,很有少场景能接受。那么换个思路,只把 html 静态部分缓存在 cdn 上呢?其实这个思路也是一个很常见的操作,即把 html 的静态框架部分缓存在 cdn 上,让用户能快速看到部分内容,然后再在客户端发起异步请求,获取动态内容并且渲染(CSR)。CSR + CDN 模式下的渲染时序图如下:
这种方式的优点是页面静态框架缓存在 cdn 上,用户可以快速看到页面框架内容,减少白屏等待焦虑。缺点是完整的页面内容需要再执行 js ,拉取异步接口回来后再进行渲染。最终有意义的动态内容展示出来的时间,比 SSR 更晚。
思路 3:ESI
CSR + CDN 的方式,很好地解决了白屏时间问题,但带来了动态内容展示的延时。之所以有这个问题,是因为我们把页面的动态内容和静态内容分割到了两个阶段中,并且是串行的,而且串行过程中还穿插了 js 的下载和执行。有什么办法把动态内容和静态内容在 CDN 上整合起来呢?
ESI (Edge Side Include) 给了我们一个很好的思路启发,ESI 最初也是 CDN 服务商们提出的规范,可通过 html 标签里加特定的动态标签,可让页面的静态内容缓存在 cdn 上,动态内容可以自由组装。ESI 的渲染时序图如下:
这个方案看起来很美好,可以把静态的部分缓存在 CDN 上了,动态部分在用户请求时会动态请求和拼接。但最关键的问题在于,ESI 模式下,最终返回给用户的首字节,还是要等到所有动态内容在 CDN 上都获取和拼接完成。也就是并没有减少白屏时间,只是减少了 CDN 和服务器之间内容传输的体积,带来的性能优化收益很小。最终效果上与 SSR 区别不大。
虽然 ESI 的效果不符合我们预期,但给了我们很好的思考方向。如果能把 ESI 改造成可先返回静态内容,动态内容在 CDN 节点获取到之后,再返回给页面,就可以保证白屏时间短并且动态内容返回不推迟。如果要实现类似于流式 ESI 的效果,要求在 CDN 上能对请求进行细粒度的操作,以及流式的返回。CDN 节点上支持这么复杂的操作吗?答案是肯定的:边缘计算。我们可以在 CDN 上做类似于浏览器的 service worker 的操作,可对请求和响应做灵活的编程。
基于边缘计算的能力,我们有了一种新的选择:边缘流式渲染方案(ESR)。方案详情如下。
渲染流程
方案的核心思想是,借助边缘计算的能力,将静态内容与动态内容以流式的方式,先后返回给用户。cdn 节点相比于 server,距离用户更近,有着更短的网络延时。在 cdn 节点上,将可缓存的页面静态部分,先快速返回给用户,同时在 cdn 节点上发起动态部分内容请求,并将动态内容在静态部分的响应流后,继续返回给用户。最终页面渲染的时序图如下:
从上图可以看出,cdn 边缘节点可以很快地返回首字节和页面静态部分内容,然后动态内容由 cdn 发起向 server 起并流式返回给用户。方案有以下特点:
- 首屏 ttfb 会很短,静态内容(例如页面 Header 、基本结构、骨骼图)可以很快看到。
- 动态内容是由 cdn 发起,相比于传统浏览器渲染,发起时间更早,且不依赖浏览器上下载和执行 js。理论上,最终 reponse 完结时间,与直接访问服务器获取完整动态页面时间一致。
- 在静态内容返回后,已经可以开始部分 html 的解析,以及 js, css 的下载和执行。把一些阻塞页面的操作提前进行,等完整动态内容流式返回后,可以更快地展示动态内容。
- 边缘节点与服务端之间的网络,相比于客户端与服务端之间的网络,更有优化空间。例如通过动态加速,以及 edge 与 server 之间的连接复用,能为动态请求减少 tcp 建连和网络传输开销。以做到最终动态内容的返回时间,比 client 直接访问 server 更快。
demo 对比
目前在 alicdn 上对主搜页面做了一个 demo (https://edge-routine.m.alibaba.com/), 下面是在不同网络(通过 charles 的 network throttle 配置限速)情况下,与原始页面的加载对比:
不限速(wifi)
限速 4G
限速 3g
5ad68981e9017.jpg">5ad68981e9017.jpg" width="auto" border="0" height="auto" alt="" title="">
从上面结果可以看出,在网速越慢的情况下,通过 cdn 流式渲染的最终主要元素出来的时间比原始 ssr 的方式出来得越早。这与实际推论也符合,因为网络越慢,静态资源加载时间越慢,对应的浏览器提前加载静态资源带来的效果也越明显。另外,不管在什么网络情况下,cdn 流式渲染方式的白屏时间要短很多。
整体架构
架构图
边缘流式渲染
1 模板
模板就是一个类似于包含 ESI 区块的语法,基于模板,会将需要动态请求的内容提取出来,把可以静态返回的内容分离出来并缓存起来。所以模板本质上定义了页面动态内容和静态内容。
在流式渲染过程中,会从上到下解析页面模板,如果是静态内容,直接返回给用户,如果遇到动态内容,会执行动态内容的 fetch 逻辑。整个过程中可能有静态和动态内容交替出现。
设计有以下几种类型的模板。
1)原始 HTML
这种模板对现有业务的侵入性最小,只需要在现有的 SSR 页面内容里加上一定的标签,即可把页面中动态部分申明出来:
-
-
-
"stylesheet" type="text/css"href="index.css"> -
"index.js" >"esr-version"content="0.0.1"/> -
-
- staic content....
-
"esr/snippet/start" esr-id="111"content="SLICE"> - dynamic content1....
-
"esr/snippet/end" > - staic content....
-
"esr/snippet/start" esr-id="222"content="https://test.alibaba.com/snippet/222"> -
"222" > - dynamic content2....
-