邓炜豪
腾讯医疗健康前端开发工程师,腾讯前端监控 Oteam PMC 成员,主要负责小程序监控系统的设计和开发。
听说代码无终点,坚持跑,就不会输。
前端性能优化的道路也没有终点,
只要坚持监控并优化,永远有下降的 “0.1s”。
背景
腾讯医药是一个专注为 C端用户提供方便、快捷、安全的在线购药、送药到家服务的医药电商平台。目前已引进诸多品牌/连锁药房,支持 B2C、O2O 购药模式,药品及各类健康品丰富多样、价格实惠、品质保障,专业的服务质量,为广大消费者的购药、用药保驾护航。
近期,在腾讯医药微信小程序上架新冠抗原检测试剂后,每日访问量暴增,用户反馈线上体验卡慢严重,急需进行前端性能优化。在多方面评估后,接入了经内部打磨多年的腾讯云前端性能监控(RUM)。
腾讯云前端性能监控(Real User Monitoring,RUM)是一站式前端监控解决方案,专注于 Web、小程序等场景监控。前端性能监控聚焦用户页面性能和质量,并且联动腾讯云应用性能观测实现前后端一体化监控。用户只需要安装 SDK 到自己的项目中,通过简单配置化,即可实现对用户页面质量的全方位守护,真正做到低成本使用和无侵入监控。
接入腾讯云前端性能监控(RUM)后的优化效果展示:
监控方案介绍
1. 指标定义介绍
按照小程序官方建议,我们将小程序的性能分为了 启动性能 和 运行时性能:
- 启动性能 :小程序的启动过程以「用户打开小程序」为起点,到小程序「首页渲染完成」为止。小程序「首页渲染完成」的标志是首个页面 Page.onReady 事件触发。
- 运行时性能:小程序的运行时性能直接决定了用户在使用小程序功能时的体验。如果运行时性能出现问题,很容易出现页面滚动卡顿、响应延迟等问题,影响用户使用。如果内存占用过高,还会出现黑屏、闪退等问题。
1.2 启动性能
以下是以小程序首页为例子,从框架侧、用户侧分析一下启动流程:
以上小程序的启动执行时机分析过后,从开发者角度而言能够优化的空间为以下步骤:
综上所述,启动性能指标集成如下:
1.3 运行时性能
运行时的性能指标,可以总结归类为以下两大类: 异常类、 体验:
综上所述,运行性能指标的集成如下:
2. 监控方案
小程序目前监控采用是自定义上报 RUM 的方案 :
关于自定义上报部分如下:
自定义上报:采用无埋点的劫持方案,代码上来说侵入性较少,可控;目前大致分为4种:
1. 生命周期上报 (劫持 App/Page 生命周期)
2. 性能上报 (wx.performance)
3. 行为事件上报 ( 劫持生命周期外函数的 event )
4. 错误上报 (RUM SDK 主动上报)
在调研分析过程中,我们遇到下列几个问题:
问题一:监控数据保存时长不能满足需求。
我们希望能保存更久的监控数据,一方面是为了把控数据存续时间,另一方面是为了方便做功能拓展,例如数据分析及告警。而在 RUM 原有的功能基础上,新增数据持久化的方案;通过 RUM 开放接口获取数据后,进行数据转化至可视化界面。
为了满足业务定制化需求,我们从 RUM 中获取了部分监控数据,制定了下列的评分机制。
最后集成出来的指标如下:
启动性能总体时间 :小程序总启动耗时 = 基本环境准备 包下载耗时 代码注入时间 首次渲染耗时;
优化过程
通过 RUM 性能评分以及微信体验评分的对比的结果,列出以下性能较差的指标;
启动性能
运行性能
通过以上列出的指标,我们接下来确定优化的重点方向为: 包下载耗时、onLaunch 、接口耗时、setData。
1. 包体积优化
可利用开发者工具的代码依赖分析与代码质量来共同查看包情况;
具体操作:
1. 删除:针对主包以及分包内无用逻辑以及业务进行确认,删除;
2. 压缩:图片压缩、 配置插件通过 treeshaking 压缩代码;
3. 整合:通过 codecc 扫描的重复率的代码,整合成方法,减少重复度;
主包体积从1.75mb 优化到 1.30 ;
mbundefined 下降约26%;启动耗时下降大概300ms;
2. 首次渲染优化
首页作为电商的门户页,承载功能较多。首屏共调用了 10多个业务接口以及日志上报等接口。接口堆积排队导致加载较慢,同时我们还发现首页实现逻辑上还有一些不合理的地方,比如业务接口数据可集成以及日志上报一条条上报。
具体操作:
1. 优先渲染骨架屏,提高首次渲染;
2. 精简首屏数据,分屏加载;
3. 异常流:数据降级处理;
2.1 优先渲染骨架屏,提高首次渲染; 通过构建骨架屏生成对应骨架,嵌入首页中,优先展示骨架屏部分,接口异步回调后渲染真实数据;
<!-- 骨架屏 --><import src="./home.skeleton.wxml"/><template is="home.skeleton" wx:if="{{loading}}" />
2.2 精简首屏数据,分屏加载;
a. 聚合接口:主页发现接口调用量超过十个, 且接口渲染数据过多而且过于分散, 聚合成2个接口;
b. 分屏加载:首屏与次屏分屏加载;
c. 队列上报:日志上报每一条上报一次,可做队列处理;
关键代码:
// 静态数据,临时方案// 静态数据,临时方案const { firstScreenData, tabData} = require('./staticData.js');/**获取页面数据*/fetchHomeData() { this.fetchFirstScreen(); //首屏:首页数据聚合到firstScreen中; this.getHomeTabData(); // 次屏:推荐商品聚合到getHomeTabData;},/**获取首屏数据*/fetchFirstScreen() { homeFirstScreen().then((res) =>{ this.initFirstScreen(res); this.createIntersectionForNav(); }). catch(() =>{ this.firstScreenFallBack(); });},/**FirstScreen 数据降级*/firstScreenFallBack() { this.initFirstScreen(firstScreenData);},
采取分屏加载流程图以及兜底:
收益对比如下:
3. setData 优化
与 Web 端不同 undefined 小程序渲染层与逻辑层为独立的线程,两边不能在内存层面共享数据,每一次 setData 逻辑层更新渲染视图都需要将数据序列化与反序列化通过数据管道传输。但是在小程序开发中 setData 反而是使用最频繁的,如果使用不当,则会极大影响用户使用体验。
监控 setData 渲染耗时的关键代码:
setUpdatePerformanceListener API 提供了 setData 相关的数据,通过 setUpdatePerformanceListener 提供的 updateEndTimestamp 和 updateStartTimestamp 可以计算出更新运算耗时,通过 dataPaths 可以获取 setData 源数据,然后通过 JSON.stringify 转换成字符串,以 JS 引擎的十六进制为标准计算数据大小。
3.1 定位发现问题
1. 在 RUM 上报完 setData 耗时监控,即可在页面性能中定位的 setData 问题。
2. 通过我们每日拉取 RUM 数据分析,目前小程序页面上 setdata 耗时较长的页面。
3.2 根据 RUM 分析现有问题
问题1:直接 setData 一个大对象,导致耗时较长。
const categorys = [...catList];const currentCategory = categorys.length > 0 ? categorys[0].category_code: null;const secondCategorys = categorys.length > 0 ? [...categorys[0].children] : [];const currentSecondCategory = secondCategorys.length > 0 ? secondCategorys[0].category_code: null;const drugListArray = [];categorys.forEach((item) =>{ if (item.children.length > 0) { item.children.forEach((subItem) =>{ drugListArray.push({ code: subItem.category_code, drugList: [], sortIndex: 0, sort: 1, isDisplay: false, }); }); } else { drugListArray.push({ code: item.category_code, drugList: [], sortIndex: 0, sort: 1, isDisplay: false, }); }});this.setData({ categorys, currentCategory, secondCategorys, currentSecondCategory, drugListArray,});
接口返回的数据量如下:
以上述 setData 耗时最长的 o2oSore 页面为例,项目中大部分 setData100 毫秒上的页面都是因为直接 setdata 了一个非常大的对象导致的。
解决方案: 对数据进行更小的分页处理,或者前端实现分段渲染,不要同时渲染过多的商品。
问题2:多次不合理的 setData
官方建议:
1. 不要在 for 循环的方法中使用 setdata 哦!
上述例子中,在一个 for 循环中执行了 setData 操作 undefined 并且多次 setData 内容都是同一个无用的。
解决方案:setData 移至 for 循环外,在数据处理完后再统一进行 setData 操作。
2. 未合并多余 setData。
解决方案:将逻辑上可以合并的 setData 进行合并,减少不必要的 setData 调用。
3. setData 中包含大量非渲染数据。
接口中请求到的数据:
前端渲染需要的数据
经分析,前端并未对渲染需要的数据进行过滤,而是全部进行了 setData 。
解决方案: 对大接口数据进行 setData 时进行渲染数据过滤,不全盘使用,保证 setData 传输的数据都是可用的。
在经过以上的优化之后,setData 的耗时从之前的 228ms 优化到了 170.60ms,下降25%;
4. 根据 RUM 地域监控数据,北方用户接口优化
优化前接口耗时分布:
问题原因 PHP 层有做三处 TKE 节点,分别是 天津、南京、广州;C 层则只有一处广州节点,所以从天津、南京过来的请求最后还是会去调用位于广州节点的 C 服务;
解决方案:逐步裁撤北方的 PHP 接入层节点
优化后接口耗时分布,北方接口耗时明显下降:
通过折线图再查看接口平均耗时收益 :
收益对比
建立好评分机制后的3月份后,发现页面评分平均分位于83分左右;
决定开始对页面开始优化,再经过持续优化后,页面评分呈上升趋势;
质量监控地址后续的几个月里稳定在92分左右;
对比下了下腾讯健康、医典等小程序的性能数据,在电商小程序还是有一些需要做的地方优化,优化点如下:
- 按需注入:启用按需注入后,小程序仅注入当前访问页面所需的自定义组件和页面代码。未访问的页面、当前页面未声明的自定义组件不会被加载和初始化,对应代码文件将不被执行。
- 初始渲染缓存:不必等到逻辑层初始化完毕,可以更早的开始渲染视图, 基础库要求2.11.1开始支持。
- 预拉取数据: 预拉取能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度。
- 分包异步化: 利用异步加载模块的方式也可以减少代码包的下载耗时和JS的注入耗时。
后续性能保障
性能优化是条长期的战线;做好后续保障和防止优化效果劣化是非常有必要的。
1. 主包的代码增量严格控制。
- 开发:可以走流水线配置包阈值,超过阈值则走;
- 线上:页面评分机制增加包大小指标;
2. 流水线
小程序代码库 CR 合流 Codecc 规则配置:
3. 总结效率高的写法文档进行宣讲定期补充。
质量跟进流程:
4. 质量跟进优化。
无法在短时间马上看到效果的优化,故需要以周为单位进行分析。并且无需进行马上发布,跟进周迭代即可;
优化评分
中位、95、99分位数性能数据:
前端性能监控相关文档推荐:
联系我们
如有任何疑问
欢迎扫码进入官方交流群~
欢迎关注腾讯云监控,了解最新动态