背景
我们负责的小程序从17年开始做一直持续到现在,页面有大几十个,内容越来越多,性能越来越差,有一段时间经常会有用户反馈卡顿、发热,非常影响用户体验。
前期有不少卡顿是能明显感知的,这部分性能问题我们已经逐步优化。明显的卡顿都已解决,剩下的性能问题有老款和低端手机才会出现、特殊场景才会出现,需要有全面的性能监控去发现这些不那么明显的性能问题。
另外我们也需要知道我们的优化策略是否正确,优化的优先级是否正确,这时如果能看到每个版本优化后的实际效果就更有目标和步骤。
现成的性能监控手段
微信小程序体系中本身有几种手段能够去做性能监控,能帮助我们解决不少问题,但都有部分缺陷,先逐一来了解下现有能直接使用的手段。
微信后台-性能监控
在微信小程序后台,开发管理-监控告警-性能监控中有 打开率、启动各阶段耗时、代码包下载量、打开耗时分布、流失时间分布,其中我们使用最多的是“启动各阶段耗时”。
优点:能够宏观的看到小程序重要阶段的耗时。
缺点:
- 数据存在时间只有60天,无法做跟长期的监控;
- 无法按页面、组件等维度来查看;
- 数据经常会刷不出来,很多时候只能查看到7天的数据;
不过启动总耗时是一个很有意义的指标,打开速度直接关系到用户流失率。这块我深有体会,一次体验线下推广时,一位老人本身手机款式较老,在加上用的4G性能,打开小程序愣是等了3~4分钟才打开,即使有线下注册礼物,用户也几乎要放弃。所以启动耗时也是我们小组每周必看的性能指标。
体验评分工具
小程序开发者工具中的体验评分也是一个性能分析的手段,可以在运行过程中实时监测,结束后给体验打分,打分的维度分为:总分、性能、体验、最佳实践。
可以作为开发过程中的辅助工具,作为发现性能和体验问题的手段。
缺点:
- 要手动运行,并且每次操作路径不同得分也不同;
- 依赖本地数据,一些线上实际场景不能覆盖;
腾讯性能狗
另外一个单机性能测试工具是 PerfDog,支持ios和安卓系统,可针对真机进行性能测试,收集的数据比较细致和完整,是一个不错的工具。
缺点:
- 单机测试,无法知道整体用户情况;
- 操作较为繁琐;
- 目前已收费;
现有的各种性能测试工具都有不少的缺陷,要么是只能针对单机、要么维度不够精细,这也是我们要自己开发性能监控的原因。
性能监控的方案
那么我们要怎么进行性能监控,监控哪些数据才有意义?
用户打开小程序的过程是,启动小程序,加载页面,页面渲染,切换页面,进行操作,退出小程序。
我们选了三个方向进行性能监控,能够监控到用户从打开到退出的各阶段性能数据。
启动耗时监控
微信官方提供了 wx.getPerformance() API,可以用于获取启动和页面切换等阶段的耗时。
启动耗时中我们上报的是启动耗时、页面首页渲染耗时和注入脚本耗时
首先需要创建一个性能监听
// 监听性能
if (wx.canIUse('getPerformance')) {
const performance = wx.getPerformance()
this.performanceObserver = performance.createObserver((entryList: WechatMiniprogram.EntryList) => {
const entryArray = entryList.getEntries()
entryArray.forEach((element: PerformanceEntryObject) => {
const {
name, duration, path, moduleName, navigationType,
} = element
duration && this.report(name, duration, {
path,
moduleName,
navigationType,
})
})
})
this.performanceObserver.observe({ entryTypes: ['navigation', 'render', 'script'] })
}
这里由于我们只监听启动阶段的耗时。
通过name属性筛选出appLaunch、firstRender、evaluateScript,依次得到启动耗时、首次渲染耗时、和脚本注入耗时。
微信也提供了 小程序测速功能,可以把 wx.getPerformance() 获取到的性能数据,通过wx.reportPerformance() 上报的微信后台,但数据只能保持7天,无法进行多个版本的性能数据对比,所以我们还是上报到自己的数据平台。
页面跳转监控
页面来回切换时的性能数据也可以通过 wx.getPerformance() 获取,可以从性能数据中筛选route、evaluateScript、firstRender可以得到页面的路由时间、脚本注入耗时、页面首次渲染时间。方法和 启动耗时 类似,就不在详细介绍。
setData性能监控
进入页面后对性能影响最大的就是错误的使用setData,主要有两个方面:
- 一是频繁 setData 导致频繁刷新出现卡顿;
- 另一个是 setData 数据量过大夹杂了很多和渲染无关的数据,导致单次 setData 耗时多久,堵塞渲染。
所以我们监控的目标也是这两个方向,频繁setData和setData数据量过大。
对setData的性能监控是通过 this.setUpdatePerformanceListener()
API来实现的。它将返回每次更新中主要更新步骤发生的时间戳,可以用来大体上估计自定义组件(或页面)更新性能。
能够获取如下字段:
每个页面、每个组件的setData 必然很频繁,如果我们都进行上报,数据量太大。我们是想要监控频繁和大数据量的setData,可以把耗费的计算资源放在端内。
我们定了两个指标:
jank_times:1秒内setData频率大于10次的次数;
stutter_times:单次setData耗时大于50ms的次数;
在小程序内监控所有的setData,只有触发这两个指标的数据我们才进行上报,在上报的数据中保留页面路径、组件路径、data 字段信息等内容,做到出现异常setData 后,能够基于这些数据进行分析和优化。
字段名 | 参数说明 | 数据类型 | 备注 |
---|---|---|---|
name | mini_set_data | string | |
page | 页面路径 | string | |
set_data_info | setData 信息 | Object | 单次上传数据不超过10M |
set_data_times | 停留页面期间 setData 总次数 | number | 页面内包括组件 setData 频率 |
jank_times | 1秒内setData频率大于10次的次数 | number | 单位时间内setData频率过高的次数(次数可配置) |
stutter_times | 单次setData耗时大于50ms的次数 | number |
set_data_info 中可以存储data的数据类型:
[{
type: "jank",
info: [{
path: '/components/cms/NewJewel/new-jewel', // 组件或页面路径
duration: 100,
size: 100,
paths: ['locationInfo', 'gpsInfo', 'shop']
}]
},{
type: "stutter",
info: {
path: '/components/cms/NewJewel/new-jewel', // 组件或页面路径
duration: 100,
size: 100,
paths: ['locationInfo', 'gpsInfo', 'shop']
}
}]
setData监控的代码可以写成 Behavior 中,组件和页面都可以使用这个 Behavior ,由于页面其实也可以使用Component 构造函数来创建,是只用特殊的组件,Behavior 中的attached 事件也会在页面中执行,可以在 attached 事件中触发 setUpdatePerformanceListener。
总体思路是在 Behavior 监控页面的onShow事件进行数据的初始化(暂存性能监控数据),onHide里面进行数据的上报,attached 事件中进行数据的监听。
this.setUpdatePerformanceListener({ withDataPaths: true }, (res: any) => {
const { updateStartTimestamp, updateEndTimestamp, dataPaths } = res
const time: number = updateEndTimestamp - updateStartTimestamp
const data: Record<string, any> = dataPaths.map((keys: string) => {
if (keys.length > 1) {
let attribute = ''
let { data } = this
for (const k of keys) {
attribute += isNumber(k) ? `[${k}]` : `.${k}`
data = data[k]
}
return {
[attribute.substring(1)]: data,
}
}
return {[keys]: this.data[keys]}
})
const size: number = JSON.stringify(data).length
})
上报策略
有了性能数据监控还得有上报,不能因为要做性能监控反而带来性能问题,所以应当尽可能小的去影响用户,只要能收集到足够分析的数据即可,所以我们的策略有两条:
- 通过接口控制是否开启性能监控;
- 通过deviceId的特殊匹配来控制用户量;
控制是否开启性能监控
这里可以用到 数据预拉取 来配置性能监控开关数据,预拉取能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度 。
wx.setBackgroundFetchToken({ token: 'performence' })
wx.getBackgroundFetchData({
fetchType: 'pre',
success(res: Record<string, any>) {
if (res?.fetchedData) {
const config: {
version: string
percent: number
enable: boolean
whiteList: number[]
} = JSON.parse(res.fetchedData)
}
},
})
用户比例控制
我们不想所有的用户都进行上报,所以想到把 deviceId 转化为数字,去数字中特定的末尾数字,比如取末尾数为9的。
小程序中的 deviceId 也是随机生成的字符串,首先需要把 hash 值转化为数字,然后再去匹配末尾数字。
const hashCode = function(str) {
var hash = 0, i, chr;
if (str.length === 0) return hash;
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash;
};
hashCode('f87dbddc-af21-479d-ac4b-30f0bb587ddf') // 1842283027
总结
通过上述三个方案,我们基本建立的自己的小程序性能监控,尤其setData的性能监控,让我们有了推进 setData 性能优化的方向,在实现上还可以定义多个 setData 频率和数据量大小指标,先消灭严重卡顿的情况,逐步调小指标,达到更优的体验。