经过优化页面性能,提高用户的体验,一直是咱们追求的目标。咱们能够经过浏览器缓存、预加载、预渲染等各类方案,来提高页面的访问性能和体验。但在实际业务场景中,有一类页面一直是性能优化的老大难,那就是首跳页面。即用户是第一次访问网站的场景。
对于 web 页面来讲,首跳场景(例如 SEO、付费引流)的性能广泛比二跳场景下要差。缘由有多种,主要是首跳用户在链接复用,和本地资源缓存利用方面,有很大的劣势。首跳场景下,不少在端上的优化手段(预加载,预执行,预渲染等)没法实施。
在客户端缓存能力没法利用的状况下,利用 cdn 距离用户近的特性,多是一个性能优化的方向。接下来将介绍几种常见的性能优化方案,并引出咱们提出的边缘渲染方案。javascript
为了性能优化考虑,咱们通常都会经过服务端渲染(SSR) ,将首屏动态内容直接服务端输出。
css
为了减小白屏时间,考虑利用 CDN 的边缘缓存能力,能够把页面 html 直接缓存在 cdn 节点上。但对于大部分场景来讲,页面的主体内容都是动态,或者个性化的,把所有 html 内容缓存在 cdn 上对于业务影响较大,颇有少场景能接受。那么换个思路,只把 html 静态部分缓存在 cdn 上呢?其实这个思路也是一个很常见的操做,即把 html 的静态框架部分缓存在 cdn 上,让用户能快速看到部份内容,而后再在客户端发起异步请求,获取动态内容而且渲染(CSR)。CSR + CDN 模式下的渲染时空图以下:
html
CSR + CDN 的方式,很好地解决了白屏时间问题,但带来了动态内容展现的延时。之因此有这个问题,是由于咱们把页面的动态内容和静态内容分割到了两个阶段中,而且是串行的,并且串行过程当中还穿插了 js 的下载和执行。有什么办法把动态内容和静态内容在 CDN 上整合起来呢?前端
ESI(Edge Side Include) 给了咱们一个很好的思路启发,ESI 最初也是 CDN 服务商们提出的规范,可经过 html 标签里加特定的动态标签,可以让页面的静态内容缓存在 cdn 上,动态内容能够自由组装。ESI 的渲染时空图以下:
java
虽然 ESI 的效果不符合咱们预期,但给了咱们很好的思考方向。若是能把 ESI 改形成可先返回静态内容,动态内容在 CDN 节点获取到以后,再返回给页面,就能够保证白屏时间短而且动态内容返回不推迟。若是要实现相似于流式 ESI 的效果,要求在 CDN 上能对请求进行细粒度的操做,以及流式的返回。CDN 节点上支持这么复杂的操做吗?答案是确定的:边缘计算。目前一些 CDN 服务商已提供完善的边缘计算能力(cloudfare已经支持,alicdn 也已有内测版本支持,并即将对外开放),咱们能够在 CDN 上作相似于浏览器的 service worker 的操做,可对请求和响应作灵活的编程。react
基于边缘计算的能力,咱们有了一种新的选择:边缘流式渲染方案。方案详情以下nginx
方案的核心思想是,借助边缘计算的能力,将静态内容与动态内容以流式的方式,前后返回给用户。cdn 节点相比于 server,距离用户更近,有着更短的网络延时。在 cdn 节点上,将可缓存的页面静态部分,先快速返回给用户,同时在 cdn 节点上发起动态部份内容请求,并将动态内容在静态部分的响应流后,继续返回给用户。最终页面渲染的时空图以下:
web
从上图能够看出,cdn 边缘节点能够很快地返回首字节和页面静态部份内容,而后动态内容由 cdn 发起向 server 起并流式返回给用户。方案有如下特色:redis
目前在 alicdn 上对主搜页面作了一个 demo (edge-routine.m.alibaba.com/)(由于 demo 页面可能会频繁), 下面是在不一样网络(经过 charles 的 network throttle 配置限速)状况下,与原始页面的加载对比:编程
从上面结果能够看出,在网速越慢的状况下,经过 cdn 流式渲染的最终主要元素出来的时间比原始 ssr 的方式出来得越早。这与实际推论也符合,由于网络越慢,静态资源加载时间越慢,对应的浏览器提早加载静态资源带来的效果也越明显。另外,无论在什么网络状况下,cdn 流式渲染方式的白屏时间要短不少。
模板就是一个相似于包含 ESI 区块的语法,基于模板,会将须要动态请求的内容提取出来,把能够静态返回的内容分离出来并缓存起来。因此模板本质上定义了页面动态内容和静态内容。
在流式渲染过程当中,会从上到下解析页面模板,若是是静态内容,直接返回给用户,若是遇到动态内容,会执行动态内容的 fetch 逻辑。整个过程当中可能有静态和动态内容交替出现。
设计有如下几种类型的模板。
这种模板对现有业务的侵入性最小,只须要在现有的 SSR 页面内容里加上必定的标签,便可把页面中动态部分申明出来:
<html>
<head>
<link rel="stylesheet" type="text/css" href="index.css">
<script src="index.js"></script>
<meta name="esr-version" content="0.0.1"/>
</head>
<body>
<div>staic content....</div>
<script type="esr/snippet/start" esr-id="111" content="SLICE"></script>
<div>
dynamic content1....
</div>
<script type="esr/snippet/end"></script>
<div>staic content....</div>
<script type="esr/snippet/start" esr-id="222" content="https://test.alibaba.com/snippet/222"></script>
<div id="222">
dynamic content2....
</div>
<script type="esr/snippet/end"></script>
</body>
</html>
复制代码
这咱模板须要单独把模板发到 cdn 上(将来若是渲染层接入了 FASS 网关和 SSR ,在这块能够和他们共用模板内容,而且在工做流中发布模板时自动同步到 cdn 上一份,同时清空 cdn 上缓存)。动态的内容有两种渲染方式。一种是利用后端 SSR 出来的动态 html 片段,另外一种是后端提供动态数据,由边缘节进行动态html片段渲染。
使用 SSR 动态 html 片段的好处是,不须要在边缘上作 html 模板渲染,而且不须要开发者写两套模板逻辑。缺点是须要后端有 SSR 能力,而且动态内容传输体积较大。
使用边缘节点渲染动态 html 内容的好处是,后端只须要提供动态数据,不须要 SSR 能力(但前端要有 CSR 的能力作降级兜底),而且传输的动态内容体积小。切点是边缘节点上没法流式透传动态内容,须要等完整下载到边缘节点上,处理后再返回给用户。
<html>
<head>
<link rel="stylesheet" type="text/css" href="index.css">
<script src="index.js"></script>
</head>
<body>
<div>staic content....</div>
<script type="esr/block" esr-id="111" content="https://test.alibaba.com/snippet/111"></script>
<div>staic content....</div>
<script type="esr/template" esr-id="222" content="https://test.alibaba.com/api/data"></script>
</body>
</html>
复制代码
静态内容来自于模板。对于不一样模板类型,获取静态内容的方式不同。对于 “原始 HTML” 类型的模板,静态内容会从首次动态请求返回的完整 HTML 中,根据 html 注释标记提取出来,并存储到 edge 缓存上。对于 “静态模板”,会经过拉取 CDN 的的模板文件 ,并存储到 edge 缓存上。静态内容有缓存过时时间和版本号。
模板一开始的静态内容会在响应时直接返回给用户。后续的静态内容(例如 html 和 body 的闭合标签)有两种方式:
a. 一种是等待动态内容返回后,再写到响应流中。这种方式对 SEO 比较友好,但缺点是动态内容会阻塞住后续静态内容,而且若是有多个动态内容区块的话,没法实现先返回的动态模板先展现,只能依次展现.
b. 另外一种方式是先把静态内容彻底返回,而后动态内容以类 bigpipe 的方式,经过脚本把内容插入到对应的坑位。这种方式的优势是静态内容能够一开始就完整展现,且多个动态内容能够先到先展现。缺点是对 SEO 不友好(由于动态内容是能进 js 插进去的)
动态内容是在渲染过程当中,解析到须要动态获取的区域,会在 edge 上发起动态内容请求。动态内容支持以动态加速的形式到达服务端(源站)。连续节点与后端的动态的内容交互,分为三种方式:
a. 第一种是后端动态内容返回的是全量的页面,须要经过注释标记来从内容中提取。这种方式的优势是对现有业务侵入较小,缺点是动态内容传输体积大,而且须要下载完整 html 后再截取动态内容;
b. 第二种是后端动态内容只返回动态区块的内容,这种方式的优势是能够将动态响应流式返回给用户,缺点时须要页面单独对外提供一个只返回动态区块内容的 url。
c. 第三种是后端动态内容只返回数据,配合静态模板中的动态渲染模板,在边缘节点上渲染出动态 html 后返回给用户。优势是与后端传输数据量小,且不须要后端有 SSR 能力。缺点是须要开发者多维护一套模板逻辑,而且在边缘节点上作复杂的模板渲染可能会有 cpu 开销和限制。
用户和边缘节点的动态内容交互,分为两种形式:
a. 瀑布流式(对应路由配置里的 WATER_FALL
): 动态内容以瀑布流的形式依次返回。虽然在边缘节点上多个动态内容加载的操做是并行的,但对于用户来讲,会从上到下依次展现页面内容。这种方式优势是对 SEO 友好,而且不影响页面模块的加载顺序。缺点是多个动态模块时,没法看到总体页面的框架,首个动态块的内容会阻塞后续动态块内容的展现,且页面底部的 js css 资源没法提早加载和执行。
b. 嵌入式(对应路由配置里的 ASYNC_INSERT
):静态内容一次性所有返回,其中动态部份内容会先占一些坑位。后续动态内容会以 innerHTML 的形式,插入到先前占的坑中。这种方式优势是页面底部的 js css 资源没法提早加载和执行,而且页面能够先看到一个全貌。缺点是对 SEO 不友好,且页面模块的执行顺序会根据动态块返回速度有所变化,须要在浏览器端页面逻辑里作一些判断和兼容。
路由配置:
g.alicdn.com/edgerender/… (只是一个设想的 url,是一个发布到静态 cdn 上的 json 资源)
{
version: '0.0.1' // 配置版本号
origin: 'us-proxy.alibaba.com',
host: 'edge.alibaba.com'
pages: [
{
pageName: 'seo', // 页面名称标识
match: '/abc/efg/.*', // 页面 path 匹配正则字符串
renderConf: { // 渲染配置
renderType: 'ESR', // 边缘渲染
templateType: 'FULL_HTML', // 模板类型:将 SSR 出的完整 html 做为模板
dynamicMode: 'WATER_FALL|ASYNC_INSERT', // 动态内容 append 返回方式:瀑布流返回|异步填坑(innerHTML)
templateUrl: '' // 模板 url
}
},
{
pageName: 'seo',
match: '/abc/efg/.*',
renderConf: {
renderType: 'ESR',
templateType: 'STATIC', // 静态模板,可经过 cdn url 获取
dynamicMode: 'WATER_FALL|ASYNC_INSERT', // 动态内容 append 返回方式:瀑布流返回|异步填坑(innerHTML)
templateUrl: 'https://g.alicdn.com/@g/xxx.html'
}
},
{
pageName: 'jump',
match: '/jump/.*',
renderConf: {
renderType: 'REDIRECT_302', // 302 跳转
rewriteUrl: 'https://jump'
}
},
{
pageName: 'proxy',
match: '/proxy/.*',
renderConf: {
renderType: 'PROXY_PASS', // 301 跳转
rewriteUrl: 'https://proxypassurl'
}
}
]
}
复制代码
路由能够认为是边缘计算的一个入口,只有在路由配置中的页面,才会走对应的渲染流程。不然页面会直接走回源,获取页面完整内容。上面的 json 是目前设计的路由配置文件。配置文件最终会在一个静态资源的方式,走覆盖式发布发到 assets cdn 上。同时,为了支持配置发布灰度,线上会存在灰度版本和全量版本的两个配置,在路由代码里配置固定比例,加载灰度或者全量版本的配置。
目前在路由里设计了三种渲染模式,分别是流式渲染、重定向和反向代理。重定向和反向代理的配置比较简单,与 nginx 配置相似,只须要提目标 url 便可。
location.reload()
的 script 标签,并结束响应,让页面强制刷新。刷新时可带上 bypass 边缘计算的 query 参数以保证刷新时不走边缘渲染先后端分离的发模式下,有一个广泛存在的问题:平滑发布。当页面的静态资源(js, css )的发布,不是与后端一块儿发布时,可能引发后端返回的 HTML 内容与前端的 js ,css 内容不匹配的问题。若是二者之间的不匹配没作兼容处理,可能会出现样式错乱或者 document 选择器找不到元素的问题。
解决平滑发布的一种方式是,在作先后端同时变动的需求时,在代码上作兼容。这样前后发布就不影响页面可用性。
另外一种方式是经过版本号。在后端页面上手动配置版本号。当有不兼容发布时,先发前端资源,而后后端手动修改版号,保证只有发布成功的后端机器, HTML 里引用的才是新版本的静态资源。
平滑发布的问题其实在分批发布和 Beta 发布的场景一直存在。只是在 ESR 的场景,咱们把静态部分缓存在 cdn 上,会使先后端不一致的可能性更大。为了解决这个问题,须要对应业务的开发者进行发布时的风险识别。若是已经作了兼容,能够不用作特殊处理。但若是没有兼容,须要在修改页面模板的版本号,新版本的动态内容,在遇到版本号不匹配的静态内容时,会放弃本次流式渲染,保证页面不出动态内容和静态内容的兼容问题。
目前各大 cdn 服务商对边缘计算的支持状况以下:
目前经过 demo,已经验证了方案的可行性。正在阿里巴巴国际站上的实际业务场景作实验。将来将会分享更完善和丰富的方案(好比直接在边缘节点上进行 react 组件渲染)和实际线上的运行效果。