共计 8245 个字符,预计需要花费 21 分钟才能阅读完成。
前言
会场是每年双十一的配角之一,会场的用户体验天然也是每年最关注的点。在日趋简单的业务需要下,如何保障咱们的用户体验不劣化甚至能更优化是永恒的命题。
往年(2020)咱们在不扭转现有架构,不扭转业务的前提下,在会场上应用了 SSR 技术,将秒开率进步到了新的高度(82.6%);也察看到在用户体验失去优化的同时,业务指标如 UV 点击率等也有小幅的增长(视不同业务场景有不同的晋升,最大可达 5%),带来了不错的业务价值。
本文将从服务端、前端两个角度介绍咱们在 SSR 上的计划与教训
- 前端在解决工程化、业务成果评估上的具体实际与方法论
- 服务端在解决前端模块代码于服务端执行、隔离和性能优化上的具体实际与方法论
(更多干货欢送关注【淘系技术】公众号)
页面体验性能的外围指标
在注释开始前咱们先介绍一下掂量的相干指标,从多年前雅虎 yslow 定义出了绝对残缺的体验性能评估指标,到起初的谷歌的 Lighthouse 等新工具的呈现,体验性能的评估规范逐步的对立且更加被大家认同。
会场的评估体系
基于 Web.Dev 以及其余的一些参考,咱们定义了本人的简化评估体系
TTFB(Time to First Byte): 第一个字节的工夫 – 从点击链接到收到第一个字节内容的工夫
FP(First Paint): 第一次绘制 – 用户第一次看到任何像素内容的工夫
FCP(First Contentful Paint): 第一次内容绘制 – 用户看到第一次无效内容的工夫
FSP(First Screen Paint,首屏可视工夫): 第一屏内容绘制 – 用户看到第一屏内容的工夫
LCP(Largest Contentful Paint): 第一次最大内容绘制 – 用户看到最大内容的工夫
TTI(Time To Interactive): 可交互工夫 – 页面变为可交互的工夫(比方可响应事件等)
大体上来说 FSP 约等于 FCP 或 LCP
会场的现状
咱们的会场页面是应用基于低代码计划的页面搭建平台产出的,一个由搭建平台产出的会场页面简略而言由两局部组成:页面框架(layout)和楼层模块。
页面框架有一份独自的构建产物(即页面的 layout html 以及根底公共的 js、css 等 assets 资源)。每个楼层模块也有独自的一份构建产物(模块的 js、css 等 assets 资源,根底公共 js 的依赖版本信息等)。
页面框架的工作比拟繁冗,负责页面的 layout、依据页面的搭投数据加载具体哪些楼层模块并组织分屏渲染模块。
会场原有的 CSR 渲染架构如下图,能够分成三局部:
- 客户端,包含手机淘宝等阿里系 App
- 文档服务,用于响应页面的主文档 HTML 清求
- 数据服务,用于响应页面的数据申请
原有的 CSR 渲染流程如下图
针对会场的性能,除了根底的大家都晓得的前端优化伎俩之外,还联合客户端能力做过很多优化计划,比拟具备代表性的有两个:
- 客户端主文档 /Assets 缓存
在客户端内,咱们利用了端侧提供的动态资源缓存能力,将 HTML 和根底公共的 JS 等资源,推送下发至用户侧客户端缓存。当客户端的 WebView 申请资源时,端侧可依据规定来匹配已下发的缓存包,在匹配胜利后间接从本地缓存中读取对应的 HTML 和 JS 资源,而无需每次都申请网络、大大缩短了页面的初始化工夫
- 数据预加载
从用户点击跳转链接到页面开始加载数据,两头还要通过客户端动画、WebView 初始化、主文档 HTML 申请以及根底公共 js 的加载和执行这些过渡阶段,加起来有 几百 ms 的工夫被节约掉。通过客户端提供的数据预加载能力,在用户点击后就能够立刻由 native 开始页面的数据加载,等页面的根底公共 js 执行完须要应用页面数据时,间接调用 jsbridge 接口即可从 native 获取曾经事后加载好的数据
在这些优化工作的根底上会场的体验性能曾经能够达到不错的水准。
随着工夫的推移,基于咱们 CSR 渲染体系下的优化存在一些瓶颈:
- 在线上简单网络环境下(低网速、虚伪的 WiFi)、Android 中低端机上的页面体验还是不尽人如意,特地是模块的加载和执行工夫比拟长,且这部分用户的占比有增长趋势
- 作为拉新的一个重要伎俩,内部唤起淘宝或者天猫客户端因为须要工夫来初始化一些性能组件,比方网络库等,页面的体验从体感上不能追平端内的会场
- 会场是营销流动性质的业务,页面的复访率绝对较低,且页面内容全面个性化。离线的 HTML 快照等用户侧缓存伎俩会因为缓存的数据过期导致呈现反复渲染(关上更慢)、页面元素跳动(渲染闪动、重排)等挫伤体验的问题
还有没有优化伎俩呢?以一个 2020 年双十一会场页面,应用 PC 上的 Chrome DevTools 的 performance 离线剖析后果为例,咱们看一下重点的问题
能够看到页面从 FP 到 FCP 这段过渡的工夫较长且只有背景色。FCP 到 LCP 这段时间处于期待图片加载的工夫,优化空间较小,且难以掂量。
离线剖析尚且如此,线上更有着简单的网络环境 / 差异化的手机机型等,这样的“背景色”工夫对用户的体验有很大的挫伤,可能会让用户更加容易跳失。
咱们的 CSR 渲染体系依赖前端 + 客户端的能力,从工作机制上曾经很难再有比拟大的晋升。怎么能力让会场页面的体验更上一层楼呢,咱们想到了服务端渲染(SSR),针对 FP 到 FCP 这段时间进行攻坚优化。
SSR 的线下测试后果,FP 到 FCP 从 825ms -> 408ms
SSR 要怎么做?
大的方向
SSR 自身意为服务端渲染,这个服务端能够在 任何中央,在 CDN 的边缘节点、在云上的核心机房或者就在你家的路由上。
实现一个 SSR 的 demo,相熟的人应该都晓得套路:
搞一个 Rax Server Renderer,传入一个 Rax Component,renderToString,完事了
业界也曾经有很多实际的案例,但就像“把大象装进冰箱里”一样,看似简略的事件在双十一所要求的简单场景稳定性下,须要有稳当可施行的执行计划。
如何在现有的这套模块化、成熟的渲染架构之上应用 SSR 呢,一开始咱们往惯例的思路去想,间接在文档 HTML 响应中返回服务端渲染实现的 HTML,看下来存在几个问题:
- 革新老本高,对现有的服务端架构改变比拟大(CDN 缓存生效,文档服务的要求更高)
- 无奈复用现有的客户端性能优化能力,比方客户端主文档 /Assets 缓存和数据预加载能力,会劣化齐全可交互工夫
- CDN 缓存无奈利用,TTFB 的工夫减少,带来了新的“齐全白屏阶段”
- SSR 服务不稳固因素较多,主动降级为 CSR 的计划简单,无奈保障 100% 可能降级
- 主文档 HTML 的平安防护能力较弱,难以抵挡黑产的歹意抓取
基于以上的问题,咱们思考是否还有其余的计划能够 低危险 、 低成本 地实现 SSR 呢?通过短暂且强烈的探讨,咱们设计了「数据 SSR」架构计划,分享给大家。
数据 SSR 渲染架构如下,文档服务返回的内容放弃动态化不变,数据服务新增调用一个独立的 SSR FaaS 函数,因为数据里有这张页面蕴含的模块列表和模块须要的数据,SSR FaaS 函数能够间接依据这些内容动静加载模块代码并渲染出 HTML。
这套计划在客户端内的场景下能够很好的将 前端 + 客户端 + 服务端三者的能力联合到一起。
有人可能会问,为什么这个计划会带来性能晋升呢?不就是把浏览器的工作移到了服务端吗?咱们举个例子(数据仅为定性分析,不代表实在值)。在失常 CSR 渲染流程下,每段耗费的工夫如下,首屏可视工夫总共耗时 1500ms。
在 SSR 渲染流程下,在 「调用 加载根底 js」之前的耗时都是一样的,因为上面两个起因,在服务端渲染的耗时会比客户端低几个数量级。
- 服务端加载模块文件比在客户端快很多,而且服务端模块资源的缓存是专用的,只有有一次拜访,后续所有用户的拜访都应用这份缓存。
- 服务端的机器性能比用户手机的性能高出几个数量级,所以在服务端渲染模块的耗时很小。依据线上理论耗时统计,服务端单纯渲染耗时均匀 40ms 左右。
因为 HTML 被放到了数据响应中,gzip 后典型值减少 10KB 左右,相应的网络耗时会减少 30~100ms 不等。最终 SSR 的渲染流程及耗时如下,能够看到 SSR 首屏的可视工夫耗时为 660ms,比 CSR 晋升了 800ms。
总而言之,「数据 SSR」的计划外围哲学是:将首屏内容的计算转移到算力更强的服务端
外围问题
大方向确定了,咱们再来看看 SSR 利用到生产中还存在哪些外围问题
- 如何做到 CSR/SSR 的平滑切换
- 开发者如何开发出“能 SSR”的代码
- 开发者面向前端编写的代码在服务端运行的不可控危险
- 低代码搭建场景下,在服务端解决楼层模块代码加载的问题
- 服务端性能
- 怎么掂量优化的价值
别急,咱们一个一个的来看解法
如何做到 CSR/SSR 的平滑切换?
在咱们的页面渲染计划中,有两个分支:
- 页面未开启数据 SSR,则与原有的 CSR 渲染流程一样,依据数据中的模块列表加载模块并渲染
- 页面开启了数据 SSR 并且返回的数据中有 SSR HTML,则应用 SSR 的 HTML 塞入到 root container 中,而后依据数据中的模块列表加载模块最终 hyrdate。
长处很显著
- 危险低,可能无缝降级到 CSR,只须要判断数据接口的响应中是否胜利返回 HTML 即可。如果 SSR 失败或者超时(未返回 HTML),通过设置正当的服务端超时工夫(例如 80ms),不会影响到用户的最终体验
- 可能利用端上成熟的性能优化能力,比方客户端缓存能力,数据预加载能力。有客户端缓存能力,页面的白屏工夫与原 CSR 统一;有了数据预加载能力,可能在页面加载之前就开始申请数据服务
在线上服务时,咱们能够通过 HASH 分桶的形式对流量进行划分,将线上的流量迟缓的切换到 SSR 技术计划,既能保障稳定性,同时还能够不便的进行业务成果的进一步评估。
比拟好的字符串转换为数字的 HASH 办法有 DJBHash,验证下来分桶成果较为稳固
开发者如何开发出“能 SSR”的代码?
很多做 SSR“demo”分享的往往会疏忽一个重要点:开发者
在双十一的场景下,咱们有百 + 的开发者,三百 + 的楼层模块,如何能推动这些存量代码降级,升高开发者的革新适配老本是咱们的一个外围方向。
咱们原有的楼层模块构建产物分为 PC/H5/Weex 三个,业界通用的是针对 SSR,独自构建一个 target 为 node 的构建产物。在理论 POC 验证过程中,咱们发现其实绝大部分的模块并不需要革新就能够间接适配 SSR,而新增构建产物会牵扯到更多的开发者,于是想找寻别的解决方案。
复用现有 Web 构建产物的一个问题是,Webpack 4 默认会注入一些 Node 环境相干变量,会导致罕用的组件库中的相似 const isNode = typeof process !== 'undefined' && process && process.env
的判断异样。不过还好这个是能够敞开的,开发环境下其余的相似 devServer
等的注入也是能够敞开的,这给了咱们一点慰藉,最终复用了 Web 的构建产物。像更新的 Webpack 5 中把 target 的差别给弱化了,也能够更好的定制,让咱们将来有了更好的社区化方向能够持续聚拢。
解决完构建产物的问题,在本地开发阶段,Rax 团队提供了 VSCode SSR 开发插件,集成了一些 best practice 以及 lint 规定,写代码的时候就能够发现 SSR 的相干问题,及时躲避和修复。
同时咱们模仿实在线上的环境,在本地提供了 Webpack 的 SSR 预览调试插件,间接 dev 就能够看到 SSR 的渲染后果。
针对开发者会在代码中间接拜访 window
、location
等变量的场景,咱们一方面开发了对立的类库封装调用抹平差别,另一方面在服务端咱们也模仿了局部罕用的浏览器宿主变量,比方 window
、location
、navigator
、document
等等,再加上与 Web 共用构建产物,所以大部分模块无需革新即可在服务端执行。
接下来的模块公布阶段,咱们在工程平台上减少了公布卡口,若在代码动态查看时发现了影响 SSR 的代码问题就阻止公布并提醒修复。
因为理论的业务模块量较大,为了进一步放大革新的范畴,测试团队联结提供了模块的批量测试解决方案。具体的原理是结构一个待革新模块的 mock 页面,通过比拟页面 SSR 渲染后的截图与 CSR 渲染后的截图是否统一,来检测 SSR 的渲染后果是否合乎预期
开发者面向前端编写的代码在服务端运行的不可控危险
只管咱们在开发阶段通过动态代码查看等办法竭力躲避问题,实际上依然存在一些针刺痛着咱们的心
- 开发者把全局变量当缓存用造成内存泄露
- 谬误的条件完结语句导致死循环
- 未知状况页面上存在不反对 SSR 的模块
这些疑难点从 SSR 的机制上其实很难解决,须要有欠缺的主动降级计划防止对用户的体验造成影响。
在说更具体的计划前要先感激咱们本人,前端曾经提前做到了 CSR/SSR 的平滑切换,让服务端能每天不活在恐怖里 = =
对于机制上的问题,能够引申浏览到之前分享过的 在 Node.js 中”绝对牢靠”的高效执行可信三方的代码。咱们这里次要聚焦在如何疾速止血与复原。
FaaS 给服务端升高了十分大的运维老本,“一个函数做一件事”的设计哲学也让 SSR 的不稳定性局限在了一块很小的局部,不给咱们带来额定的运维累赘。
低代码搭建场景下,在服务端解决楼层模块代码加载的问题
业界分享的一些 SSR 场景根本都是整页或者 SPA 类型的,即 SSR 所应用的 bundle 是将整页残缺的代码构建后暴露出一个 Root Component,交由 Renderer 渲染的。而咱们的低代码搭建场景,因为整个可选的模块池规模较大,页面的楼层模块是动静抉择、排序和加载的。这在前端 CSR 状况下很不便,只有有个模块加载器就能够了,然而在服务端问题就比较复杂。
还好咱们的模块标准恪守的是非凡的 CMD 标准,有显式的依赖关系申明,能够让咱们在获取到页面的楼层组织信息之后一次性的把页面首屏的全副 Assets 依赖关系计算出来。
在服务端加载到代码后,咱们就能够拼装出一个 Root Component 交给 Renderer 渲染了。
服务端性能
性能上次要是有几个方面的问题
- 机制问题
- 代码问题
机制问题
因为楼层模块很多,在理论执行的过程中发现存在一些机制上的性能问题
- 代码的 parse 工夫较长且不稳固
- 流量较低状况下难以触发 JIT
优化计划的话比拟 tricky
- 缓存
vm.Script
实例,防止反复 parse - 冀望一致性 HASH 或主动扩缩容(本次未实现)
巡检的时候还观测到存在小范畴的 RT 抖动问题,剖析后定位是同步的 renderToString 调用在宏观上存在排队执行的问题
在这种状况下会造成局部渲染工作的 RT 为多个排队工作的渲染 RT 叠加,影响单个申请的 RT(但不影响吞吐量)。这种问题要求咱们须要更准确的评估备容的资源。机制上无效的解法揣测能够让 renderToString
以 fiber 的形式执行,缓解宏观排队造成的不偏心的问题。
代码问题
性能问题的剖析当然免不了 CPU Profile,拿出最爱的 alinode 进行剖析,很快的能够找到热点进行针对性优化。
上图中标蓝的办法为 CMD 加载器计算依赖的热点办法,对计算结果进行缓存后热点打消,整体性能晋升了 80%》.》
怎么掂量优化的价值
这么多的投入当然须要欠缺的评估体系来进行评估,咱们从体验性能和业务收益两个别离评估。
体验性能
基于兼容性较好的 PerformanceTiming
(将被 PerformanceNavigationTiming 代替),咱们能够获取到前端领域下的一些要害的工夫
- navigationStart
- firstPaint
其中 navigationStart
将会作为咱们的前端终点工夫所应用。在前端之外,对用户的交互门路而言真正的终点是在客户端的点击跳转工夫 jumpTime
,咱们也联结客户端进行了全链路埋点,将客户端 native 的工夫与前端的工夫串联了起来,纳入到咱们的评估体系中。
在最开始的外围指标中,咱们看到有 FCP、TTI 这几个指标。目前的 Web 实现中,还未有兼容性较好的能够线上掂量的计划(线下能够应用 DevTools 或者 Lighthouse 等工具),因而咱们通过其余的形式来做近似代替
线上取到的数据通过 tracker 的形式进行无采样上报,最终咱们能够通过多个维度进行剖析
- 机型
- 网络条件
- 是否命中 SSR
- 是否命中其余前端优化
次要的掂量指标有
- 从用户点击到 FCP 的工夫(FCP – jumpTime)
- 从 NavigationStart 到 FCP 的工夫(FCP – NavigationStart)
业务收益
这部分很忐忑,体验的优化是否会带来真金白银的收益呢?咱们间接通过 AA 和 AB 试验进行业务数据的剖析。
基于之前的切流分桶,咱们能够通过相似 hash 值 % 10
的形式将流量分为 0~9 号十个桶,首先通过 AA 试验验证分桶是否平均
统计指标举例
这一步是保障分桶的逻辑自身不会有数据的歪斜影响置信度。接下来咱们再进行 AB 试验,逐渐减少试验桶验证业务数据的变动。
最终的成果
搞了这么多,得看看产出了。在这次双十一会场中,咱们切流了多个外围的页面,拿到的第一手数据分享给大家。
小米 5 骁龙 820 处理器
能够看到,在 Android 碎片化的生态的下,带来的晋升甚至超出了预期,这也给了咱们将来更大的能源,将前端 + 客户端 + 服务端的能力更无效的联合到一起,带给用户更好的体验,给业务发明更大的价值。
将来的渲染架构还会更简单吗?
为了更好的用户体验,当然会了!咱们能够简略的看看短期和长期的一些事件
电商体验指标的对立定义
长期以来,业务在用户侧的实现有 Web、Native、Hybrid 混合开发等多种抉择,每个体系都有着本人的关闭体验衡量标准,这就造成了一些“鸡同鸭讲”的问题。而 Web.dev 中所定义的 FCP、LCP 通用评估体系也并不适宜电商场景,能展现出外围的商品 / 店铺其实对一张页面来说就实现了它的使命。
后续咱们能够将体验指标评估规范对齐,将终点工夫、绘制实现工夫等在多个体系对齐概念与实现,达到相互之间能够横向比拟良性竞争的状态。
工程上还有更多的事件要做 …
在 Webpack 5 的 Release Note 中,咱们能够看到 Webpack 正在弱化 target 的一些非凡解决,将 Web 形容为了 browserlike 的环境。同时还提供了自定义 browserlist 的能力,能够给予开发者更不便解决跨端的兼容性问题的能力。这一变动将推动咱们更快的拥抱社区,取得更好的开发体验。
现有的 SSR 动态代码查看计划会有一些漏网之鱼,还有没有更欠缺的计划能从工程上前置解决代码危险(性能、平安)问题也是将来的一个方向。
ServiceWorker Cache 等离线缓存快照
复访率高,变动不太大的页面能够利用 ServiceWorker Cache 等计划,将之前的渲染后果缓存下来,命中缓存间接用,未命中缓存 SSR。升高服务端压力的同时能够让体验更好。
SSR 的性能优化与平安
现阶段的 Node.js 或者说 V8,对于动静加载代码的状况反对并没有特地的欠缺,缺失了平安相干的爱护逻辑。并且从性能上来说,SSR 属于 CPU 密集型的 workload,纯异步的劣势并不显著,也可能须要一些非凡的解决方案来配合。
内部投放场景的笼罩
「数据 SSR」的计划是端内的最佳计划,却是外投场景的最劣计划。外投场景下因为用户是在第三方 App 中关上页面,相应的缺失了客户端的定制化优化能力,SSR 调用会造成数据服务的 RT 减少,反而推后了 FCP。
这时候古老的 HTML 直出计划又能够再捞回来了。
外围在于
- 利用 CDN 的边缘计算能力,能够较好的做到“动静拆散”以及容灾
- 应用中心化的 SSR 函数,能够将 SSR 的不稳定性与 CDN 的可靠性拆散,保障近端链路的牢靠,避免出现近端间接不可用导致的无奈复原
近端的流式计划常常被提及,然而在理论的应用中会遇到当流式输入遇到谬误时,用户侧无奈无效容灾的问题(HTML 损毁,无奈补救)。通过“动静拆散”能够将页面分为
仅将 Root Container 进行动态化,进而在享受流式输入带来的 TTFB 提前的益处的同时又能兼顾容灾 SSR 的不稳固。和业务团队更能够一起探讨下如何将页面更好的从业务上做到“动静拆散”,而不是仅从技术的角度登程。
总结
渲染架构的不断改进本质上是咱们在无限且变动的环境下(终端性能、简单网络和多变业务)自发做的适应,兴许有那么一天,环境不再是问题,性能优化的课题将会隐没。咱们项目组有时候还开玩笑,等明年手机叒换代了,5G 100% 遍及了,是不是这些优化都能够下线了????
然而!当初看现实还有点远,在 2020 的双十一会场咱们走进了一个新的深水区,期待将来技术与业务结合能带给宽广用户更棒的体验!