本文来自飞猪前端@聆乐同学,通过对于飞猪双十一会场中使用 SSR 进行加速的一个全面总结,希望可以给到大家输入,欢迎一起交流~

缘起

曾经在PC时代通过模板同步渲染实现页面秒出基本是标配,但由于移动端的网络和运行环境条件受到地域,设备的影响参差不齐,再加上手机相比于电脑的屏幕差距对页面的响应要求更高,为了减少页面大小以及配合离线技术和前后端分离,移动端H5页面静态资源分离和异步渲染逐渐成为默认的标准
而为了满足在2G,3G的弱网条件页面可用性,提高响应速度,静态资源离线成为移动端体系下一个黑科技,并逐渐发展为各个App的通用能力,有了离线能力及时是在弱网或者无网的情况下,依然可以给到用户响应(无网下是一个打底页),离线能力在过去至少5年的时间一直是移动端H5技术体系下不可或缺的一环
另一方面从12年all in 无线以来,移动端的环境也逐渐发生着巨大的变化,手机硬件的升级日新月异,配合着webview的优化,页面的渲染不断加速,4G的普及和5G的到来也让网络延迟降到个位数毫秒级,于此同时我们也在不断思考在未来网络和终端下前端页面的渲染应该是什么样的?后离线时代我们的备选方案是什么?
于是Serverless Faas和React/Rax SSR的结合实现页面的同步渲染成为我们的一个探索方向,Node Faas SSR同构实现一套代码两端运行解决复用的问题,Serverless的低运维,弹性伸缩解决SSR对机器Cpu消耗高的难题,所以今年9月开始我们在调研之后,选择对接淘系提供的比较成熟的营销域SSR方案在飞猪双十一会场落地实践

当前营销体系的问题

1. 整体链路耗较长

image.svg
基于上图可以大致看出,目前飞猪营销搭建的页面真实的渲染并不是直接在接口数据返回后执行的,还需要拉取依赖的模块缓存,然后再进行页面的render,整个过程串行进行,耗时很长且不稳定
image.svg 从今年国庆会场的整体性能数据上也可以看出,首屏模块load耗时 Android 500-600ms、iOS 400-500ms, 首屏mtop耗时均值在 300ms左右,整体首屏时间 Android 2s左右、iOS 1.5s左右,所以用户进入页面到真的能够看到页面,基本需要在1s以上,这还是在端内有接口prefetch和模块缓存的数据,在端外体验问题更加严峻 而现在基于各类数据统计都告诉我们页面加载时间增加,会导致更多的用户流失

2. 页面snapshot的不足

为了减少页面loading过长的情况,营销页面做了首屏html的本地缓存,使得用户再次进入页面时减少等待,但缓存方案存在以下问题

  • 缓存的方案需要用户进入过一次页面,并成功缓存了html才有效,所以该方案无法解决用户第一次进入页面时等待时间较长的问题,且由于使用缓存,若用户清除缓存或是缓存存储失败,那仍然无法减少用户后续进入时的等待时长。
  • 缓存只能存储上一次进入的数据信息,而目前飞猪营销页面,大量的投放数据都是基于个性化算法投放,缓存数据与真实数据的不一致会使页面重新渲染,尤其是当页面模块有明显变化时,重新渲染带来的页面闪烁感会更明显。

    SSR的方案设计

    基于以上的问题,我们希望能够通过SSR将html片段生成放在服务端执行,以此优化渲染时间,主要分析如下

  • 客户端渲染快慢非常受容器影响,也就是我们常说的高端机快,低端机慢,但是当渲染放在了服务端上,这个容器的影响就可以规避

  • 从飞猪链路上可以看到,容器渲染前模块load的时间消耗,而这块耗时平均在300ms上下,端侧渲染,每一次http请求的建立都会有较长的时间损耗,而在服务端渲染,这段时间的消耗可以通过服务器来避免
  • 由于纯粹的SSR直出页面对定位和个性化场景的改造较大以及和离线,prefetch体系的不兼容,所以我们选择借助天马的异步ssr的方案,在现有的搭建链路上,增加ssr render的链路,在mtop数据中返回首屏的Html Str直接插入到页面中,这样在端内可以借助离线和prefetch加速页面秒出的过程

    MTOP:阿里内部移动端统一的API网关

image.svg

  • 优点: 链路改造成本低,模块基本复用,可使用端上现有的离线包、prefetch等优化手段,页面url无变化,走mtop链路无需额外处理数据安全
  • 缺点: 接口耗时增加,因为在mtop中返回html,数据量和耗时都会升高

    落地过程的疑难问题

    1. 图片闪烁

  • 原因: 飞猪模块中,有许多模块直接使用了rax的原生picture组件,基础组件在web渲染时会根据端判断是否支持webp而加上webp的后缀,而在ssr渲染时,具体的端判断为false,所以ssr下图片无webp后缀,图片链接的变化导致了图片闪烁

  • 改造方案: 不再直接使用基础picture组件,而是使用业务定制组件,确保图片的剪裁、后缀拼接在ssr及web上保持一致

    2. 沉浸式titlebar的支持

    飞猪营销页面中,尤其是大促时的会场页面,会有大量的沉浸式titlebar使用场景,而前端titlebar的实现,是通过桥方法的调用,由容器告知原生titlebar以及状态栏的高度,而在ssr渲染时,服务器无法通过桥与容器进行交互拿到状态栏高度,这成为了ssr支持沉浸式的一个问题

方案探索:
  • 方案一(不可行):发起mtop前,在页面调用桥信息获取titlebar高度,通过mtop传递给ssr服务层,但是由于页面会有prefetch的开启,所以端上会直接请求mtop,而不是一定先经过页面的,所以会导致prefetch的页面没有titlebar信息带给mtop
  • 方案二:从solution执行链路上发现其实ssr返回的html在注入页面后,到web的真实render之间是会有一个空隙的,而这段时间是已经在真实的web层的,是可以获取到真实的容器titlebar高度信息的。

    实际改造方案:

  • 在ssr层,我们根据容器、以及沉浸式参数匹配,在ssr层注入titlebar的占位dom,并给上一个默认高度

  • 在web层,ssr html注入完成后,获取真实的titlebar高度,dom操作原有的默认高度,将其变为真实的占位高度,使其和web时渲染的titlebar高度一致 ```javascript // 示例代码 // server render function renderTitlebarP(type, stickyPaddingTop) { return (
    1. {type === 1 ? <View
    2. className="ssr-title-bar-main"
    3. style={{
    4. position: 'fixed',
    5. top:0,
    6. height: 0,
    7. left: 0
    8. }} /> : null}
    9. <View className="title-bar ssr-title-bar" style={{
    10. width: '750rpx',
    11. height: stickyPaddingTop + 'rpx',// stickyPaddingTop 为不同状态titlebar时的默认高度
    12. position: 'relative'
    13. }} />
    ) }

// web render if (如果有html返回) { const pageContent = document.getElementsByClassName(‘mui-zebra-page-content’)[0]; pageContent.innerHTML = html;

  1. // ssr的html注入后页面后,获取真实的不同机型的titlebar(含状态栏)的高度stickyPaddingTop

// dom操作ssr的html ssrTitlebar.style.height = ${stickyPaddingTop}px; } // 拉取模块,并render web fetchAndEvalJS(seedComboUris, { sequence }).then(() => {render()}) 复制代码

  1. <a name="TRYfQ"></a>
  2. #### titlebar改造引发新的问题
  3. - 页面闪烁
  4. - setAttitude的使用:改造方案中由于对dom操作中使用setAttitude进行了style的改变,但是由于rax组件在生成时会有一些属性的注入,使用setAttitude会造成style的结构变化,触发重选渲染,因此出现页面闪烁
  5. - 解决:改用 A.style.xxx 来最小力度改变想改的属性,避免重排
  6. - 页面模块重复
  7. - dom不一致:titlebar的判断中,使用了大量的端判断,而端判断是基于容器的ua,而在ssr层通过mtop获取的ua是不一样的,导致titlebar的判断上端判断不一致,因此返回的dom不一致,而这里需要提到rax的hydrate,他是通过比对dom顺序进行,导致如果渲染的两份dom顺序不一致,就会出现两份dom的重复情况
  8. ```javascript
  9. // mtop ua
  10. "MTOPSDK/1.9.3.48 (iOS;14.0;Apple;iPhone11,8)"
  11. // window
  12. "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
  13. 复制代码

3. 离线包

  • 飞猪离线原理,统一渲染页html离线,ssr作为页面开关会,当命中飞猪离线时会以离线的统一渲染页的页面开关为准
  • 增加ssr上的白名单链接配置,确保只有指定的页面会真的进行ssr 链路的render

    页面效果

    | 无ssr | 有ssr | | —- | —- | | nossr.gif | ssr.gif |

会场SSR数据表现

线上数据

根据线上页面的数据对比,我们发现开启了SSR的会场的可见时间都在1s以下,极大优于传统的首屏时间

同一页面(wifi下)实验数据

从整体会场性能和单页面的对比可以看到,异步ssr的方案会带来mtop耗时的上升,因为html在接口中返回,增加了接口数据大小,但是由于直接返回了html,节省了原有的模块拉取再render页面结构的时长,所以整体的页面可见时间仍然较原来有了提升,基本都能达到1s内可见

iphone8 安卓(p30 pro)
开启ssr 未开启ssr 开启ssr 未开启ssr
首屏mtop耗时 首屏时间 首屏mtop耗时 首屏时间 首屏mtop耗时 首屏时间 首屏mtop耗时 首屏时间
507 1123 268 1057 374 796 336 1277
887 1463 569 1252 385 859 304 1115
372 973 328 1196 402 1083 293 1256
322 815 491 1195 387 826 298 1370
1058 1557 398 1548 414 918 494 1560
446 851 248 867 464 1063 248 1117
355 837 279 1062 403 838 496 1418
372 792 332 1344 447 1021 286 1163
420 841 316 1024 380 863 297 1282
399 1230 276 2531 372 844 333 1355
均值 513.8 1048.2 350.5 1307.6 402.8 911.1 338.5 1291.3

服务端相关

性能和稳定性

  • RT:SSR服务耗时之前压测时平均在30 - 40ms,线上由于页面复杂度不同稍有变化,目前日常在49ms
  • Cpu:SSR是密集计算型操作会比较耗Cpu,所以相比普通函数,会给SSR函数提供更多的机器量
  • 弹性伸缩:第一次压测有验证弹性缩扩容的能力,在弹起的瞬间失败率上升较快,而且由于CSE双十一降级自动扩容能力,所以我们最终使用机器固定容量,并采用单机限流的方式
  • 超时容灾:SSR作为会场性能的增强能力,为了在极端情况下不影响页面的功能,我们设置了SSR的60ms超时,也就是超过60ms的请求走正常的CSR渲染链路
  • 降级预案:为了应对突发情况,配置了关闭整体SSR的预案开关,用作紧急情况的降级

    后续规划

    1. 同步SSR支持

  • 目前方案的首屏可见时间虽有优化,但仍然受限于接口传输的影响,而端外场景无法使用任何其他优化手段减少接口传输的时长,后续会在端外尝试直接服务端渲染方案,而不是通过接口返回html方案

    2. 页面AB效果

  • 目前页面ssr功能开启仅通过开关开启,并未支持同一页面同时开启ssr/关闭ssr的ab能力,后续AB能力的支持来做更有效的业务效果对比

    3. SSR能力搭建基础化

  • 作为页面性能优化的基础能力提供给搭建页面使用,减少现在白名单配置以及人肉check的成本

    4. 性能优化

  • SSR在服务端执行的速度还需要持续优化,另外函数迁移到FC利用其弹性扩容的能力解决机器不足和浪费问题

    总结

    在当前前端多端多适配技术体系下,客户端预渲染、Weex、Flutter可能都难以成为解决多端秒出的方案,通过拥抱原生html渲染技术配合Faas SSR面向未来网络条件布局,可能是前端页面性能往前再走一步的尝试突破,而这也是在Serverless概念热火朝天的当下云+端的最佳实践之一,从试点到通用再到普惠,刚刚走出第一步,前路漫漫仍需努力