小程序优化
1. 性能收集
1.1 方式选择
- 开发者工具:开发者工具可以查看性能评分,并给出优化建议。
- We分析:微信小程序官方工具,可以查看小程序性能大盘,如冷启动,页面加载时间等。
- 自定义埋点:在小程序中添加自定义埋点,收集性能数据,如页面加载时间,接口请求时间等。
2. 收集方式
2.1 接口性能
在请求拦截器里面,统一封装逻辑,记录每个接口的请求性能,和请求正确性。
并根据配置,决定是否上报数据。
- 收集的方式:
wx.request
支持传入enableProfile
参数,开启后会在请求完成后返回请求的性能数据。- 对于慢接口(如大于3000ms),则一定上报错误,并标记是否命中上报概率。
- 对于正常接口,若命中上报概率,则上报数据。
- 基于此规章,则可以计算出慢接口的比例,以及所有接口的性能详情。
2.2 冷启动/页面加载时间
- 为了做到无侵入方式收集冷启动时间,则需要重写App函数,在生命周期内收集页面性能指标。
// 保存原始的App函数
const originalApp = App;
// 重写App函数
App = function(appOptions) {
// 添加性能收集逻辑
const originalOnLaunch = appOptions.onLaunch || function() {};
const originalOnShow = appOptions.onShow || function() {};
// 记录应用启动时间
const launchTime = Date.now();
appOptions.onLaunch = function(options) {
// 执行其他逻辑
// 执行原始的onLaunch
originalOnLaunch.call(this, options);
};
appOptions.onShow = function(options) {
// 执行其他逻辑
// 执行原始的onShow
originalOnShow.call(this, options);
};
// 使用原始App创建应用
return originalApp(appOptions);
};
- 调用
wx.getPerformance
获取性能数据。
查看官方文档
const performance = wx.getPerformance()
const observer = performance.createObserver((entryList) => {
console.log(entryList.getEntries())
})
observer.observe({ entryTypes: ['render', 'script', 'navigation'] })
3. 埋点SDK设计思路
支持不同端的上报:
- 适配器模式:适配不同的端,如微信小程序、支付宝小程序、H5端,不同的端调用不同性能收集方式。
- 抽象工程模式:负责根据运行环境,创建不同的适配器实例。则上报逻辑无需考虑端的差异。
支持批量上报或单条上报:
- 若是批量上报,则维护一个上报队列,则根据一定的策略进行上报
- 达到10条数据时上报
- 每10秒上报一次
- 小程序进入后台时上报
- 若是单条上报,则直接上报数据。
- 一般来说,如果是产品需求埋点,则采用单条上报方式,若是性能数据,则采用批量上报方式。主要是因为性能数据量大,且由于IOS无法监控“墓碑模式”,导致可能无法监控切后台操作。
- 若是批量上报,则维护一个上报队列,则根据一定的策略进行上报
可选上报的数据类型:
上报采用websocket进行通信,提供protobuf协议进行数据传输,或原始数据传输,在开发阶段采用原始数据传输,生产环境采用protobuf协议进行数据传输,便于调试观察。重试逻辑:
- 若上报失败,则重试3次,每次间隔1秒。
支持上报不同的数据类型:
- 性能数据
- 包括冷启动时间、页面加载时间、接口请求时间等。
- 自定义性能数据:如webview加载时间
- 业务埋点需求
- 由产品定义的埋点需求,若是页面访问事件,则采用无感收集。
- 性能数据
数据报表: 将埋点数据推送到数据报表平台(如阿里云SLS平台),制作自定义报表。通过观察数据趋势或对异常数据的告警,形成闭环。
4. Sentry
4.1 集成方式
SDK选择
sentry 不支持小程序,有人提供了开源的sentry-miniapp,基于Sentry模块进行封装,以适配小程序环境。Source-map 上传 通过Taro CLI 或 miniProgram CLI, 在构建完后,将source-map文件上传到Sentry服务器。然而该source-map是不包括源码的source-map,所以不能直接在sentry里面定位源码,还需要结合开发者工具定位源码代码。
注意:一般来说,sentry使用source-map 是 hide-source-map 类型,包含原始代码
4.2 分包异步化
分包异步化 由于sentry sdk,占用了主包大小体积,所以需要将sentry sdk放在分包中。但是在分包未下载之前,无法自动上报或收集错误。
所以分包加载成功之前,需要手动搜集错误,并在分包加载成功后,再手动交给Sentry上报。
分包异步化方式
由于Taro对分包异步化支持有限。github issue
- 在构建时,将Sentry SDK 排除到主包外。
- 通过Rollup或webpack配置,将Sentry SDK独立打一个分包,使用
__non_webpack_require__.async
进行异步加载。
坑点:使用小程序构建小程序代码之前,一定要先保证分包已经成功打包并置于dist源码目录下。 补充说明,Taro的构建流程:源码 ——> Taro 构建 ——> dist目录下 ——> 小程序构建 ——> Taro CLI 推送最终代码到微信服务器。
4.3 告警上报
- Sentry支持自定义告警规则,但是前提是Sentry需要将错误分类好。所以上报时候,需要调用Sentry API将错误归类。
- Sentry支持配置webhook,我们可以配置飞书或钉钉的webhook,将错误信息推送到飞书或钉钉群中。
- Sentry支持错误统计报表,我们可以在Sentry里面定义报表,如接口超时报表、JS错误报表等。
- 错误闭环:Sentry上报支持自定义字段,如我们可以上报用户的SessionId,用于定位问题。
5. We分析
5.1 集成方式
We分析不需要主动集成(也可以显示调用API),在小程序内置了We分析SDK。我们只需要在we分析平台开通相关服务即可。
5.2 功能
- 业务数据(产品方向)
- 性能大盘 (页面性能,启动性能)
- 自定义性能:有单条上报,流程上报,模块上报三种方式,单条上报如直接上报接口性能,流程和模块上报,可以上报整个支付链路的性能
- 可视化日志:若开启后,小程序可以可视化查看用户的日志,为了与业务数据管理,可以调用We分析API,上报SessionId等信息。
- JS错误统计
- 实时日志:可以通过We分析API上报实时日志,便于调试和定位问题。
6. 性能调试
6.1 开发者工具
- 性能评分:开发者工具可以查看小程序的性能评分,并给出优化建议。
- 性能分析:开发者工具可以查看小程序的性能分析,包括冷启动时间、页面加载时间、接口请求时间等。
- 性能监控:开发者工具可以查看小程序的性能监控,包括内存使用情况。(不能看到总体数据,只能看到JS占用内存)
- 性能优化建议:开发者工具可以根据小程序的性能数据,给出优化建议,如减少页面渲染时间、减少接口请求时间等。
6.2 第三方App真机测试
- 如preDog等第三方App,可以在真机上测试小程序的性能数据。
- 可以准确知道小程序每个页面占用的内存。由于小程序使用的 同层渲染技术, ,还能知道小程序对宿主App(微信)的影响,如视频组件对宿主App内存的影响。
6.3. 云测试
- 微信云测试平台,可以在云端进行小程序的性能测试。
6. 性能优化
6.1 减少冷启动时间
- 使用分包: 独立分包、分包预下载、分包异步化等方式,减少主包体积。
6.2 接口优化
- DSN预请求
- 在小程序管理后台,配置预请求地址,小程序在启动时会自动请求该地址,DNS获得缓存。
- HttpDNS
- 购买HttpDNS服务,小程序在请求接口时会使用HttpDNS解析域名,减少DNS解析时间。
- 实际测试下来表明,确实减少了DNS解析时间,尤其是在网络不稳定的情况下。但是增加了总体请求时间,因为HttpDNS需要先请求DNS服务器获取域名解析结果。(最终放弃)
- 域名合并
- 通过域名收敛,减少DNS解析次数和建立TCP链接次数。
- 数据预拉取
- 对于首页配置,每个人用户都是一样的配置,在小程序冷启动时,就可以同步请求数据。文档说明
- 接口缓存
- 对于不会变更的数据,可以使用接口缓存,减少接口请求次数。(如城市数据)
6.3 页面加载优化
- 使用内存缓存,如将localStorage中的数据同步到内存中,减少读取localStorage的时间。
- 使用虚拟列表,减少页面渲染时间,尤其是列表数据较多的情况下。
6.4 代码优化
略。可以参考普通h5页面的代码优化方式,如减少DOM操作、减少JS计算量、媒体资源压缩等。
7. 一个宣传页优化的思路
我们小程序首页是一个宣传页,展示车辆卖点的宣传页。里面大概有20多屏的内容,每一屏可能是视频或者是图片,或者是轮播视频。里面还包括了动画效果,随着滚动播放动画。
第一版思路:图片采用懒加载,视频采用预加载。通过Observe监听判断目标元素距离屏幕的距离。有个阈值,当目标元素距离屏幕的距离小于阈值时,则加载该元素的资源或者开始播放动画效果。
当离开屏幕后,则停止播放动画效果,或者暂停视频播放。
但是,我们发现这样实现后,发现低端的苹果手机的小程序在频繁切换页面后,某个页面,页面所有的操作都无法响应。
然后我们发现一个规律,就是会提示内存不足,然后页面生命周期函数会触发多次,而且每次切换页面触发次数会不断累加。
这个bug,最开始以为是Taro框架的bug,通过其他小程序的测试和社区寻找,发现是IOS小程序架构的的bug,是当内存不足时,IOS会将其他页面销毁。当再回复的时候,状态无法完全恢复,导致页面生命周期函数被多次触发且事件无法响应。
然后我们就想办法测试内存到底是多少,发现开发者工具的内存只能监控JS堆栈的内存。we分析的内存监控也不准。最终我们找到了一个第三方桌面应用,可以监控小程序webview的内存和微信的内存占用。
最终发现视频组件占用了大量微信的内存,而没占用小程序的内存。
然后我们就想办法优化视频组件的内存占用。 方式1:更换了视频的编码使用了H265,去除音频轨道。 方式2:视频不在可视区域中,直接停止播放,再用封面图替换视频组件。
通过上面方式,发现有所缓解内存,但是还是会出现内存不足的情况。
第二版思路:采用虚拟列表的方式,通过虚拟列表销毁不在可视区域的元素。 然后设置每个元素块的背景色,避免滚动过快产生的白屏加载问题。
发现这个优化能显著降低内存占用,尤其是视频组件的内存占用。
但是在低端手机上会出现动画播放不流程的问题,尤其是快速滚动虚拟列表的时候卡顿更明显。
考虑到小程序的架构是双线程架构,JS线程和渲染线程分离。JS线程负责逻辑处理,渲染线程负责页面渲染。
我们通过重写setData计算每次耗时的事件,在快速滚动的时候,setData耗时很高,甚至超过了100ms。 在快速滚动的过程中,事件触发过多,JS线程无法及时处理,导致渲染线程无法及时更新页面。
然后我们就想了一个办法,将这个虚拟列表中所有的事件,合并为一个事件中心管理,事件中心维护的是一个数组,每一屏都在一个数组项里面维护,每次都是修改事件中心的状态,如果事件中心状态变更,然后每30ms再触发一次事件中心的状态。整个渲染的逻辑更新都依赖于事件中心的状态。
甚至我们做了更绝的是,低端机,直接放弃使用动画,直接显示动画终态。
后面,产品发现动画维护成本太高,每次更换宣传图,都要适配效果,然后最终又放弃了动画效果。