得物App中嵌入了大量的前端Web页面用以承接各种灵活多变的业务场景和玩法,但因为众所周知的原因,Web应用的用户体验是很难与原生应用相比的。然而,随着搭建器功能的不断完善,支持的业务场景和组件也越来越多,越来越多的团队和部门优选使用搭建器搭建会场页面投放于得物App当中,这对搭建器的整体用户体验提出了更高的要求。
从我开始接触搭建器后,看到了很多搭建器项目为了用户体验优化所做的一些努力与优秀的解决方案,这些方案在各自的应用场景当中发挥了极其重要的作用。因此,抽时间以前端开发人员的视角梳理了现有的一些优秀方案,一则作为知识沉淀留档,方便之后查阅,二则也可以给后来者一些参考与借鉴。
谈到用户体验,肯定首先要做的就是梳理衡量/验收指标以及当前瓶颈,这样才能做到有的放矢,针对高优的体验瓶颈进行针对性的优化,以最小的成本换取最大的收获。
说到体验指标,或许每个公司都有不同的定义与口径,但无论如何变化,始终离不开以下几点核心要素:
结合上述核心要素,在得物中落地时被转化为以下指标:
秒开率是衡量H5打开速度的重要指标。在业界,普遍会使用FMP(全称 “First Meaningful Paint”,翻译为“首次有效绘制”)表示页面的“主要内容”开始出现在屏幕上的时间点, 秒开率基本等同于FMP。得物的秒开率计算方式为:count_if( webview启动时间 + FMP时间 < 1000) / count(*)
业界方案
秒开率的统计与上报,绕不开FMP指标的计算与统计,我们参考了业界的一些现有方案,并结合业务特点设计更贴合我们业务的FMP计算公式。
两个方案大致相同都是基于权重计算出关键dom。通过mutationobserver来监听变化,记录对应时间;然后在渲染结束后筛选出比较重要的dom, 再用这些dom拿到对应的耗时。
他们区别如下:
dom类型:svg、canvas、img、video、object、embed
const IGNORE_TAG_SET = ["SCRIPT", "STYLE", "META", "HEAD", "LINK"];
// 如果一个页面内有一个容器,容器内有多张图片,图片的重要应该高于容器, 这个值不宜设置过小,
// 否则会出现大多数场景body就是权重最大的元素,而不是里面的图片元素。
// 至少应该在 3*3,2*5 这样的布局中权重最大的元素为其中最后显示的图片,
// 由于存在空隙、文本等其他元素,大致为 40
const TAG_WEIGHT_MAP = {
SVG: 60,
IMG: 60,
CANVAS: 60,
OBJECT: 120,
EMBED: 120,
VIDEO: 120
};
// 普通节点权重:1
// 权重计算公示:width*height*weight,有背景图片的 div 等同于 img 标签
我们的方案
我们的方案大致和上文中提到的一致,部分细节做了一些适配和优化。
文章中提到的dom变动的计算方式有些问题,两个相加的方式会造成误差比较大。因此我们选择使用 performance.mark来计算,不过隐藏的问题是这个只是资源加载的时间,没有包含渲染的时间,数值会偏小。
由于cat-design(内部组件UI库)对图片有CDN裁剪优化,我们需要把图片处理成去掉这些参数后的形式,以免资源名称不一致。
抖动率是衡量一个页面是否稳定的核心指标,如果打开页面后,页面上的模块一直频繁变换,用户体验无疑极差的。因此,我们也得关注页面的CLS指标,防止大范围频繁抖动。后续也会对项目中针对页面抖动的优化做详细的介绍。
由于用户设备所处的环境千奇百怪,可能是设备兼容性问题,也可能是网络问题,纯粹通过数据的统计,总是可能出现一些疏漏,并且缺乏对用户实际体验的真实反馈。为了补足这一部分可能缺失的数据,我们在一些用户访问频繁的核心频道页面,如:天天领券、疯狂周末、随心省 等页面设置了用户体验调查问卷,让有反馈需求的用户可以在这边反馈他们所遇到的体验问题:
通过用户反馈的一些高频体验问题,我们会针对性地进行排查可能导致问题的原因。
例如:
当然,很多时候,出现这些问题,不一定是代码实现有问题,有可能确实是用户的设备老旧,渲染性能和运行内存较低或者是用户所处的网络环境不佳(如在电梯中)导致的一些体验问题。因此用户的这些体验调查,仅作为体验指标统计的补充,我们的优化依然还是主要围绕着体验指标数据进行,再辅以用户反馈高频问题的排查以达到最真实的用户体验优化效果。
确定了体验指标和优化的方向之后,我们再来具体的看一下应该如何针对这些指标进行针对性的优化。
在绝大部分性能体验优化中,静态资源的优化都是首当其冲的,因为这个优化的效果往往是最为直接的,并且优化起来也是比较容易的,没有太多的弯弯绕绕,只需要想办法「降体增速」即可。
文档类资源指的是html、js、css等文件,这类的文件通常生成之后都是固定的,我们通常可以利用以下方式进行优化:
除了上述通用优化策略外,我们通常还需要对html文件进行进一步的优化,原因主要是:
因此,如果我们想要最大限度的利用上html文件,那么就需要解决以下两个问题:
我们针对上述两个问题逐个分析,逐个解决
当然,我们需要注意,尽可能只是获取与首屏展示相关的信息,非首屏展示相关的不要再服务端渲染,不然会导致html体积增大从而影响资源响应速度。
使用SSR之后,html的有效信息确实是得到提升了,但CDN加速对SSR并不友好,CDN更适合用于缓存加速一些静态资源,而针对SSR这种动态资源有点力不从心。但如果我们想要资源响应速度得到进一步的提升,CDN又是不可或缺的一环。
因此,我们需要更近一步,从SSR变为SSG,从服务端渲染到服务端生成,也就是说,我们在使用SSR拿到了首屏渲染的html字符串后,不再是直接返回给浏览器,而是将其导出成html文件,并上传至CDN,这样就能够充分利用CDN 的加速能力加速首屏html的获取了。
不过我们使用SSG+CDN虽然达到了提速的目的,但是有个场景的问题不容忽视:针对不同用户、人群有不同展示的个性化组件。由于CDN缓存是没有状态和身份的,因此,所有用户访问的内容都是一样的,此时我们就没办法针对不同的用户在首屏渲染时展示特异性的数据。
基于上述原因,我们决定对组件进行分类:
通用骨架屏:针对有实验、目标人群、逻辑动态显示/隐藏的组件,在SSG阶段时不再直接按照接口返回数据展示,而是展示一个通用的骨架屏,当到了用户设备浏览器中进行客户端渲染时(此时可以拿到用户身份),再对骨架进行数据填充完成渲染。
我们上面的用户满意度调查当中,有一项是“图片不出来”,而从收集上来的用户反馈来说,图片加载问题其实反馈还是挺频繁的。再加上我们大部分的组件都需要通过图片的方式为用户提供更加丰富的表达,因此,对于图片类资源的优化也是很有必要的。
图片类资源也属于静态资源,因此同样可以使用上面文档类资源使用的一些优化方案,如:CDN加速、缓存策略、图片压缩等。除此之外,我们还需要针对图片资源进行更细粒度的优化。
通常我们在开发时,为了确保图片在高清屏不会模糊,我们下载下来的图片一般都是多倍图(搭建器这边通常用的是3倍),但如果在一些非高清屏护着是屏幕分辨率较低的设备上,下载多倍图无疑是画蛇添足的,不仅没能达到更好的展示效果,还可能出现锯齿,同时使得资源下载时间变得更长,推迟了用户看到图片的时间。
我们期望的效果是:在浏览器请求图片资源时,需要根据当前设备的分辨率、DPI等屏幕信息,选择最优的图片尺寸和清晰度,从而减少在低端设备图片下载的体积,提升下载速度,又能确保在高清设备当中能够展示高清图。
因此,在搭建器当中,我们封装了一个自定义的Image组件,当传入的图片是符合预设域名要求时,我们将会给图片链接上加上如下请求参数:
这个参数是CDN服务器为我们提供的将图片转换为webp格式的参数,当带有这个参数的图片请求到服务器后,服务器给我们返回的格式便是webp。
或许有同学会说,webp好像并不是所有设备都支持吧,那如果在不支持webp的设备,图片不是就展示不了了?
确实,因此我们的Image组件经过多轮改造以确保图片在不同设备中均能正常展示:
版本1:
<picture>
<source srcset="https://h5static.dewucdn.com/node-common/bbdb0b2c-8549-b2cf-ceb8-62b98de2c983-1125-984.jpg?x-oss-process=image/format,webp/resize,w_750" type="image/webp" />
<img src="https://h5static.dewucdn.com/node-common/bbdb0b2c-8549-b2cf-ceb8-62b98de2c983-1125-984.jpg" alt="" />
</picture>
我们使用picture去加载图片,如果支持webp的设备,就使用webp,不支持的话,就还是用兜底的原图。但这个方案在IOS设备上会同时加载webp和原图,造成不必要的流量损耗和占用浏览器并行下载数,后来被废弃。
版本2:
try {
window._promiseimgWebpError = new Promise(function(resolve,reject){
var img = new Image();
img.src = 'data:image/webp;base64,UklGRkwAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAIAAAAAL1ZQOCAkAAAAUAEAnQEqAQABAAFAJiUAToAoAAD+8iJYwmknR5t5G30DAAAA'; // 替换为小webp的base64
img.onerror = function() {
resolve('小webp图片error')
};
})
} catch (e) {
console.error(e);
}
// ...
window._promiseimgWebpError.then(() => {
const { src, type, options } = this.props;
this.setState({
localStr: transformSrc(type, src, options, undefined, false),
});
});
在这个版本中,我们尝试在浏览器中加载一个很小的webp 图片,如果加载失败,就说明当前设备不支持webp图片,我们就会使用兜底的原始图片。这种方式的检测,就不会出现在IOS设备同时加载两种格式图片的情况,又可以确保在支持webp的设备展示webp ,不支持的设备展示兜底图。
静态资源优化后,会场页面的整体体验已经得到了极大的提升了,绝大部分情况下用户访问页面时,能够以最快的速度获取到html文档和图片资源。
但是,还是有一些情况会导致首屏页面加载体验下滑,经过分析,这些体验下滑的会场有以下特点:
上述两个问题都出现在「组件交付接口」上:
因此,要解决这两个问题,搭建器这边提出了:「接口聚合」、「接口前置」的概念。
接口聚合主要是为了解决一个页面中存在多个依赖组件交付接口的组件时,需要发起多次组件交付接口造成的抖动以及网络资源的浪费问题。核心的实现思路就是:
就如上文所说,浏览器请求组件交付接口需要等待:文档下载、html解析、main.js执行、组件交付接口等流程,出现了较长时间的滞后,如果我们可以把这个请求交付接口的阶段提前,放到文档下载之后,无疑是可以让用户能够更快的看到核心内容的。
上面两个接口优化,都是在h5层面上的优化,始终还是得经历「webview启动 -> 下载html」这样的一个过程,如果html体积偏大,那么这期间也是会产生一定的耗时的。为了在一些特定场景能够跨越这一个看似无法逾越的天堑。h5团队联合native团队一起,设计了一套 「接口预请求」机制,期望将首屏数据请求进一步的提前,在native打开webview的同时就并行地发起请求。
有了这样的预请求机制,我们首屏页面所依赖的接口数据返回的时间又可以缩短很多,让我们这些页面的首屏渲染体验达到最佳。
上图中提到了一个“竞速”机制,即哪个返回比较快就用哪个,但后续数据验证客户端请求在99%的情况下是快于h5的请求的,并且接口竞速在会场会有去重问题,因此目前最新的方案是使用的是等待超时走h5请求的兜底逻辑。
上面我们分别从资源和接口层面尝试优化了从用户请求到实际展示内容的链路,让用户能够尽早的看到核心内容。接下来我们再来看一下当页面到达了浏览器进行CSR(客户端渲染)后的用户体验优化。
对于一些跟用户无关,所有用户都展示一样的组件,我们在进行SSG生成html文件时,实际已经获得了这些组件的核心数据了,那么此时用户一打开网页,看到的实际上就是我们之前已经获取好的这些数据展现的组件样式。这样一来,用户一进入页面,白屏的时间几乎可以忽略,差不多一进来就可以看到一些内容。只需要等CSR的时候接口返回的数据去更新一下一些差异即可,对用户来说前后的变化比较小,从感官上就像是一打开就看到了实际内容一样。
如果某些组件的展示严重依赖于用户身份的,像上面所说的, CDN中无法识别用户身份,此时我们只能展示一个通用的骨架,至少让用户知道有这么一个模块,并且防止CSR后展示了这个模块后出现较严重的页面抖动。等待CSR接口返回之后,我们再去替换这个骨架完成渲染。
上面说的SSR占位和骨架屏填充还有一个比较严重的体验问题需要解决:
由于在得物 App中,很多组件都会设置AB实验或者是某些组件只是针对特定人群展示,如:新客。而在CDN中拿到的缓存页面,实际上是区分不了人群和用户身份的,就会导致在CDN缓存中的页面,不知道究竟是否应该展示这个组件,如果展示了,到了客户端发现当前用户不应该展示,就会像上述视频一样出现刚开始有个模块,CSR之后消失的情况。如果不展示,到了客户端返现当前用户应该展示时,又会导致凭空多出一个组件把下面的组件直接往下挤的抖动情况。
针对这种情况,我们针对这种根据用户信息判断是否要展示的组件,在服务端渲染时,都将组件的高度默认设置为0,等到了客户端渲染时,如果发现当前组件需要展示,那么再将这个组件的高度设置为auto ,而为了让高度变化时不会突然变化,让用户看起来特别奇怪,我们为这个组件的高度变化设置了渐变过渡,让其逐步展开。就这样,一个原本看起来是极为生硬,体验拉胯的页面,经过改造之后,就变成了好像是精心设计好的动画一样,毫无违和感。
经过上面几轮的优化之后,我们会场页面的用户体验可以说又上了一个台阶。当然,我们进行上述优化的过程中,也产生了一些副作用。我们先来看几张图:
CSR渲染流程
SSR渲染流程
我们可以看到,从我们将CSR渲染首屏换成SSR渲染首屏后,TTFB 变得比以前更长了,即在用户访问页面到页面文档返回的时间变长了。
原因是因为我们在 SSR 渲染阶段,需要获取页面全量组件的数据并将其渲染成 HTML,而每个组件的数据获取都需要一定的耗时,从而导致我们最终获取到HTML的时间拉长。当然,我们上面说的SSG + CDN的方案可以很大程度上缓解用户可感知的等待时间,但每次CDN回源时依然还是需要走SSR的流程,TTFB的变长终归对用户体验有一些影响。
恰巧最近比较火的「流式渲染」就能够解决上述痛点,因此,团队也尝试在流式渲染的方向上摸索前进,预计达到的效果:
接入流式渲染的页面,TTFB将会得到很大的降低,用户能够感知的白屏时间也被最大限度的缩短,并且可以利用浏览器空闲时间,高效且并行的进行多组件异步加载,哪个组件先加载好久展示哪个,没有加载好之前,依然可以展示骨架屏兜底展示,防止页面抖动。
目前搭建器组件有100多个,涉及到的业务领域包括但不限于营销、交易、增长等多个业务域的20余组件开发者,每个双周迭代都会有大量的组件业务迭代需求。面对这如此密集的业务迭代以及涉及众多业务域的影响范围,倘若组件没有进行较为完善的容错机制,其中的某一个组件因为某个版本的改动而出现异常,就极有可能导致该页面的其他组件也受到影响,最严重的可能导致整个页面白屏。
本着「敬畏线上,谨慎编码」的原则,需要一个比较完善的组件容错机制和告警机制,一来确保即使某个组件出现严重Bug时不影响页面其他组件的正常工作,二来我们可以第一时间感知组件出现的异常,及时排查,修复止损。
在搭建器的组件渲染时,为每一个组件的渲染单独包裹了一个错误边界组件,这个组件将会捕获当前组件的异常和错误,防止该错误继续往上冒泡影响到页面其他组件。这样就可以将当前组件的错误影响范围始终都限制在组件范围内,而不会扩大影响其他组件。
而当我们捕获到异常时,我们会直接隐藏这个组件,这样就可以避免因出现异常而导致组件渲染混乱而影响用户的使用。
在上面捕获到异常之后,我们会将捕获到的组件异常上报到监控平台并告警,这样,一旦正式环境有某些组件因业务迭代改动导致异常时我们可以第一时间感知,并及时处理。
至此对于搭建器的用户体验优化已经告一段落了。但我们还需要想办法对后续的业务迭代的体验劣化进行管控。就算你这一次体验做得再好,经过几轮业务迭代之后,可能体验又大幅下滑了。
因此,我们期待通过一些手段来防止前端页面的体验劣化。
得益于现成的体验卡口平台:体验卡口平台
我们只需要基于这个平台进行一定的改造和功能新增,就可以对我们关注的体验指标进行细粒度检测,如:接口前置、图片转webp、接口响应时间等等。后续我们还会不断的丰富检测能力,支持流式检测、ssr检测等等,尽可能通过这个平台的检测与管控,防止前端页面体验下滑。后续也可能做成强卡形式,如果高优体验问题不解决,禁止上线,以此保障前端页面的交付质量。
经历了上面这些体验后,是否真的达到了我们的预期呢?我们是不是身处于自身描绘的理想环境当中,而真正的用户体验不增反降呢?这一切的一切,都需要用实际的数据说话。
首先,从我们的核心体验指标“秒开率”看一下:
从对秒开数据的统计来看,虽然每次版本迭代都有不同程度的上下波动,但整体趋势上还是稳步提升的,由此也可以看出,我们在用户体验上的优化,至少在秒开率上是得到了正向的反馈。
从抖动率的指标来看,进行优化后项目的稳定率整体长期保持在99.5%左右,由此可看出对于页面抖动相关的优化以及在开发时有意识地避免一些可能出现抖动的技术方案还是颇有成效的。
而收集上来的用户体验报告来说,正向反馈还是占了绝大多数的。由此可见,我们的优化成果,不仅仅是我们单方面的臆想,而是实实在在能让用户感受出来的体验提升。当然,其中仍有一小部分问题反馈,我们也会持续跟进,在业务迭代之余,逐步优化体验,力求为用户提供最佳的使用体验。
至此就算梳理完了当前搭建器及其关联项目在用户体验优化上的一些实践了。这些实践大部分都是我加入团队之前,团队的其他同学就已经完成的。当然,我也参与了其中一部分功能的开发与优化。
总的来说,团队对于用户体验的优化是孜孜不倦的,力求给用户最好的体验,促使用户能够顺利在平台上“得到好物”。
文 / 星河
关注得物技术,每周、更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。