前言

What,啥事边缘计算?今日早读文章由1688高级前端工程师,全栈开发者@黑卡、@镜逸,@霸天授权分享。

正文从这开始~~

背景

通过优化页面性能,提升用户的体验,一直是我们追求的目标。我们可以通过浏览器缓存、预加载、预渲染等各种方案,来提升页面的访问性能和体验。但在实际业务场景中,有一类页面一直是性能优化的老大难,那就是首跳页面。即用户是第一次访问网站的场景。

对于 web 页面来说,首跳场景(例如 SEO、付费引流)的性能普遍比二跳场景下要差。原因有多种,主要是首跳用户在连接复用,和本地资源缓存利用方面,有很大的劣势。首跳场景下,很多在端上的优化手段(预加载,预执行,预渲染等)无法实施。

在客户端缓存能力无法利用的情况下,利用 cdn 距离用户近的特性,可能是一个性能优化的方向。接下来将介绍几种常见的性能优化方案,并引出我们提出的边缘渲染方案。

思路思路 1:SSR

为了性能优化考虑,我们一般都会通过服务端渲染(SSR) ,将首屏动态内容直接服务端输出。

vue刷新页面数据丢失_vue刷新页面_vue.js 页面刷新问题

这种方式的优点时一次 html 返回即可包含页面主体内容,不需要浏览器二次请求接口后再用 js 渲染。但这种方式的缺点也比较明显,对于距离服务端远,或者服务端处理时间较长的场景,用户会看到较长时间的白屏。而且即使 html 返回完成了,用户并不会立即看到内容,页面还需要加载前置的 js,css 等资源后,才能看到内容。SSR 模式下的渲染时空图如下:

思路2 – CSR + CDN

为了减少白屏时间,考虑利用 CDN 的边缘缓存能力,可以把页面 html 直接缓存在 cdn 节点上。但对于大部分场景来说,页面的主体内容都是动态,或者个性化的,把全部 html 内容缓存在 cdn 上对于业务影响较大,很有少场景能接受。那么换个思路,只把 html 静态部分缓存在 cdn 上呢?其实这个思路也是一个很常见的操作,即把 html 的静态框架部分缓存在 cdn 上,让用户能快速看到部分内容,然后再在客户端发起异步请求,获取动态内容并且渲染(CSR)。CSR + CDN 模式下的渲染时空图如下:

vue刷新页面_vue刷新页面数据丢失_vue.js 页面刷新问题

思路 3:ESI

CSR + CDN 的方式,很好地解决了白屏时间问题,但带来了动态内容展示的延时。之所以有这个问题,是因为我们把页面的动态内容和静态内容分割到了两个阶段中,并且是串行的,而且串行过程中还穿插了 js 的下载和执行。有什么办法把动态内容和静态内容在 CDN 上整合起来呢?

ESI (Edge Side Include) 给了我们一个很好的思路启发,ESI 最初也是 CDN 服务商们提出的规范,可通过 html 标签里加特定的动态标签,可让页面的静态内容缓存在 cdn 上,动态内容可以自由组装。ESI 的渲染时序图如下:

vue刷新页面_vue.js 页面刷新问题_vue刷新页面数据丢失

这个方案看起来很美好,可以把静态的部分缓存在 CDN 上了,动态部分在用户请求时会动态请求和拼接。但最关键的问题在于,ESI 模式下,最终返回给用户的首字节,还是要等到所有动态内容在 CDN 上都获取和拼接完成。也就是并没有减少白屏时间,只是减少了 CDN 和服务器之间内容传输的体积,带来的性能优化收益很小。最终效果上与 SSR 区别不大。

虽然 ESI 的效果不符合我们预期,但给了我们很好的思考方向。如果能把 ESI 改造成可先返回静态内容,动态内容在 CDN 节点获取到之后,再返回给页面,就可以保证白屏时间短并且动态内容返回不推迟。如果要实现类似于流式 ESI 的效果,要求在 CDN 上能对请求进行细粒度的操作,以及流式的返回。CDN 节点上支持这么复杂的操作吗?答案是肯定的:边缘计算。我们可以在 CDN 上做类似于浏览器的 service worker 的操作,可对请求和响应做灵活的编程。

基于边缘计算的能力,我们有了一种新的选择:边缘流式渲染方案(ESR)。方案详情如下。

渲染流程

方案的核心思想是,借助边缘计算的能力,将静态内容与动态内容以流式的方式,先后返回给用户。cdn 节点相比于 server,距离用户更近,有着更短的网络延时。在 cdn 节点上,将可缓存的页面静态部分,先快速返回给用户,同时在 cdn 节点上发起动态部分内容请求,并将动态内容在静态部分的响应流后,继续返回给用户。最终页面渲染的时序图如下:

vue刷新页面数据丢失_vue刷新页面_vue.js 页面刷新问题

从上图可以看出,cdn 边缘节点可以很快地返回首字节和页面静态部分内容,然后动态内容由 cdn 发起向 server 起并流式返回给用户。方案有以下特点:

demo 对比

目前在 alicdn 上对主搜页面做了一个 demo (), 下面是在不同网络(通过 charles 的 network throttle 配置限速)情况下,与原始页面的加载对比:

不限速(wifi)

vue刷新页面_vue.js 页面刷新问题_vue刷新页面数据丢失

限速 4G

vue.js 页面刷新问题_vue刷新页面_vue刷新页面数据丢失

限速 3g

vue.js 页面刷新问题_vue刷新页面_vue刷新页面数据丢失

从上面结果可以看出,在网速越慢的情况下,通过 cdn 流式渲染的最终主要元素出来的时间比原始 ssr 的方式出来得越早。这与实际推论也符合,因为网络越慢,静态资源加载时间越慢,对应的浏览器提前加载静态资源带来的效果也越明显。另外,不管在什么网络情况下,cdn 流式渲染方式的白屏时间要短很多。

整体架构架构图

vue.js 页面刷新问题_vue刷新页面数据丢失_vue刷新页面

边缘流式渲染模板

模板就是一个类似于包含 ESI 区块的语法vue刷新页面,基于模板,会将需要动态请求的内容提取出来,把可以静态返回的内容分离出来并缓存起来。所以模板本质上定义了页面动态内容和静态内容。

在流式渲染过程中,会从上到下解析页面模板,如果是静态内容,直接返回给用户,如果遇到动态内容,会执行动态内容的 fetch 逻辑。整个过程中可能有静态和动态内容交替出现。

设计有以下几种类型的模板。

原始 HTML

这种模板对现有业务的侵入性最小,只需要在现有的 SSR 页面内容里加上一定的标签,即可把页面中动态部分申明出来:

  1. <link rel="stylesheet" type="text/css" href="index.css">

  2. <script src="index.js">

  3. <meta name="esr-version" content="0.0.1"/>

  4. staic content....


  5. <script type="esr/snippet/start" esr-id="111" content="SLICE">

  6. dynamic content1....

  • <script type="esr/snippet/end">


  • staic content....


  • <script type="esr/snippet/start" esr-id="222" content="https://test.alibaba.com/snippet/222">

  • <div id="222">

  • dynamic content2....

  • <script type="esr/snippet/end">

  • 静态模板(暂时没有关联的实际场景)

    这种模板需要单独把模板发到 cdn 上(未来如果渲染层接入了 FASS 网关和 SSRvue刷新页面,在这块可以和他们共用模板内容,并且在工作流中发布模板时自动同步到 cdn 上一份,同时清空 cdn 上缓存)。动态的内容有两种渲染方式。一种是利用后端 SSR 出来的动态 html 片断,另一种是后端提供动态数据,由边缘节进行动态html片断渲染。

    使用 SSR 动态 html 片断的好处是,不需要在边缘上做 html 模板渲染,并且不需要开发者写两套模板逻辑。缺点是需要后端有 SSR 能力,并且动态内容传输体积较大。

    使用边缘节点渲染动态 html 内容的好处是,后端只需要提供动态数据,不需要 SSR 能力(但前端要有 CSR 的能力做降级兜底),并且传输的动态内容体积小。切点是边缘节点上无法流式透传动态内容,需要等完整下载到边缘节点上,处理后再返回给用户。

    1. <link rel="stylesheet" type="text/css" href="index.css">

    2. <script src="index.js">

    3. staic content....


    4. <script type="esr/block" esr-id="111" content="https://test.alibaba.com/snippet/111">


    5. staic content....


    6. <script type="esr/template" esr-id="222" content="https://test.alibaba.com/api/data">

    7. <div>

    8. {$data.name}

    9. </div>

    静态内容展现

    静态内容来自于模板。对于不同模板类型,获取静态内容的方式不一样。对于 “原始 HTML” 类型的模板,静态内容会从首次动态请求返回的完整 HTML 中,根据 html 注释标记提取出来,并存储到 edge 缓存上。对于 “静态模板”,会通过拉取 CDN 的的模板文件 ,并存储到 edge 缓存上。静态内容有缓存过期时间和版本号。

    模板一开始的静态内容会在响应时直接返回给用户。后续的静态内容(例如 html 和 body 的闭合标签)有两种方式:

    一种是等待动态内容返回后,再写到响应流中。这种方式对 SEO 比较友好,但缺点是动态内容会阻塞住后续静态内容,并且如果有多个动态内容区块的话,无法实现先返回的动态模板先展示,只能依次展示。

    另一种方式是先把静态内容完全返回,然后动态内容以类 bigpipe 的方式,通过脚本把内容插入到对应的坑位。这种方式的优点是静态内容可以一开始就完整展示,且多个动态内容可以先到先展示。缺点是对 SEO 不友好(因为动态内容是能进 js 插进去的)。

    动态内容

    动态内容是在渲染过程中,解析到需要动态获取的区域,会在 edge 上发起动态内容请求。动态内容支持以动态加速的形式到达服务端(源站)。连续节点与后端的动态的内容交互,分为三种方式:

    第一种是后端动态内容返回的是全量的页面,需要通过注释标记来从内容中提取。这种方式的优点是对现有业务侵入较小,缺点是动态内容传输体积大,并且需要下载完整 html 后再截取动态内容。

    第二种是后端动态内容只返回动态区块的内容,这种方式的优点是可以将动态响应流式返回给用户,缺点时需要页面单独对外提供一个只返回动态区块内容的 url。

    第三种是后端动态内容只返回数据,配合静态模板中的动态渲染模板,在边缘节点上渲染出动态 html 后返回给用户。优点是与后端传输数据量小,且不需要后端有 SSR 能力。缺点是需要开发者多维护一套模板逻辑,并且在边缘节点上做复杂的模板渲染可能会有 cpu 开销和限制。

    用户和边缘节点的动态内容交互,分为两种形式:

    瀑布流式(对应路由配置里的 WATER_FALL ): 动态内容以瀑布流的形式依次返回。虽然在边缘节点上多个动态内容加载的操作是并行的,但对于用户来说,会从上到下依次展示页面内容。这种方式优点是对 SEO 友好,并且不影响页面模块的加载顺序。缺点是多个动态模块时,无法看到整体页面的框架,首个动态块的内容会阻塞后续动态块内容的展示,且页面底部的 js css 资源无法提前加载和执行。

    嵌入式(对应路由配置里的 ASYNC_INSERT ):静态内容一次性全部返回,其中动态部分内容会先占一些坑位。后续动态内容会以 innerHTML 的形式,插入到先前占的坑中。这种方式优点是页面底部的 js css 资源无法提前加载和执行,并且页面可以先看到一个全貌。缺点是对 SEO 不友好,且页面模块的执行顺序会根据动态块返回速度有所变化,需要在浏览器端页面逻辑里做一些判断和兼容。

    边缘路由

    路由配置:

    1. {

    2. version: '0.0.1'//配置版本号

    3. origin: 'us-proxy.alibaba.com',

    4. host: 'edge.alibaba.com'

    5. pages: [

    6. {

    7. pageName: 'seo', //页面名称标识

    8. match: '/abc/efg/.*', //页面path匹配正则字符串

    9. renderConf: {

    10. //渲染配置

    11. renderType: 'ESR', //边缘渲染

    12. templateType: 'FULL_HTML', //模板类型:将SSR出的完整html作为模板

    13. dynamicMode: 'WATER_FALL|ASYNC_INSERT', // 动态内容append返回方式:瀑布流返回|异步填坑(innerHTML)

    14. templateUrl: ''// 模板url

    15. }

    16. },

    17. {

    18. pageName: 'seo',

    19. match: '/abc/efg/.*',

    20. renderConf: {

    21. renderType: 'ESR',

    22. templateType: 'STATIC', // 静态模板,可通过cdn url获取

    23. dynamicMode: 'WATER_FALL|ASYNC_INSERT', // 动态内容append返回方式:瀑布流返回|异步填坑(innerHTML)

    24. templateUrl: 'https://g.alicdn.com/@g/xxx.html'

    25. }

    26. },

    27. {

    28. pageName: 'jump',

    29. match: '/jump/.*',

    30. renderConf: {

    31. renderType: 'REDIRECT_302', // 302跳转

    32. rewriteUrl: 'https://jump'

    33. }

    34. },

    35. {

    36. pageName: 'proxy',

    37. match: '/proxy/.*',

    38. renderConf: {

    39. renderType: 'PROXY_PASS', // 301跳转

    40. rewriteUrl: 'https://proxypassurl'

    41. }

    42. }

    43. ]

    44. }

    路由可以认为是边缘计算的一个入口,只有在路由配置中的页面,才会走对应的渲染流程。否则页面会直接走回源,获取页面完整内容。上面的 json 是目前设计的路由配置文件。配置文件最终会在一个静态资源的方式,走覆盖式发布发到 assets cdn 上。同时,为了支持配置发布灰度,线上会存在灰度版本和全量版本的两个配置,在路由代码里配置固定比例,加载灰度或者全量版本的配置。

    目前在路由里设计了三种渲染模式,分别是流式渲染、重定向和反向代理。重定向和反向代理的配置比较简单,与 nginx 配置类似,只需要提目标 url 即可。

    稳定性

    影响范围控制

    异常处理

    灰度

    1)边缘计算代码灰度

    本身平台支持灰度发布边缘计算代码。

    2)路由配置灰度

    在边缘计算代码里,根据固定比例,加载灰度版本和正式版本的两个配置 url。灰度发布时只发布灰度配置,全量发布时发布全量配置。发布的同时清空 cdn 缓存。

    3)页面内容灰度

    给灰度页面一个特殊的模板版本号,遇到这个版本号的话,就不走边缘渲染。

    平滑发布

    前后端分离的发模式下,有一个普遍存在的问题:平滑发布。当页面的静态资源(js,css )的发布,不是与后端一起发布时,可能引起后端返回的 HTML 内容与前端的 js,css 内容不匹配的问题。如果两者之间的不匹配没做兼容处理,可能会出现样式错乱或者 document 选择器找不到元素的问题。

    解决平滑发布的一种方式是,在做前后端同时变更的需求时,在代码上做兼容。这样先后发布就不影响页面可用性。

    另一种方式是通过版本号。在后端页面上手动配置版本号。当有不兼容发布时,先发前端资源,然后后端手动修改版号,保证只有发布成功的后端机器, HTML 里引用的才是新版本的静态资源。

    平滑发布的问题其实在分批发布和 Beta 发布的场景一直存在。只是在 ESR 的场景,我们把静态部分缓存在 cdn 上,会使前后端不一致的可能性更大。为了解决这个问题,需要对应业务的开发者进行发布时的风险识别。如果已经做了兼容,可以不用做特殊处理。但如果没有兼容,需要在修改页面模板的版本号,新版本的动态内容,在遇到版本号不匹配的静态内容时,会放弃本次流式渲染,保证页面不出动态内容和静态内容的兼容问题。

    边缘 cdn 服务商

    目前各大 cdn 服务商对边缘计算的支持情况如下:

    alicdn

    akamai

    cloudfare

    落地计划

    我们会在一个典型的首跳场景进行实验。目前已经在灰度上线,通过 webpagetest 在印尼测试进方案和不进方案的对比,可以看出优化效果:

    webpagetest 对比结果:

    参考

    关于本文

    限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
    站长微信:ziyuanshu688