1. 背景
达达集团是中国领先的本地即时零售与配送平台,达达快送小程序作为公司重要的 ToC 业务入口,对外提供帮取帮送、跑腿帮买、帮排队等服务,承担了 ToC 业务较高部分的订单量。对于小程序而言,性能至关重要。
面临什么问题
用户打开率偏低。以微信平台为例,打开率是指在小程序冷启动的情况下,点击小程序到首屏渲染完成的百分比。代码包越大,启动耗时越长,白屏时间越久,用户越可能因为失去耐心而退出小程序,在转化率不变的情况下,订单量越低。
性能不达标。如果性能不达标会带来以下两点影响:
影响小程序流量获取
影响用户体验,进而降低用户留存
不同平台小程序性能指标构成不尽一致,但主要有三个维度考虑:启动性能、网络性能、运行性能,其中网络性能主要包含请求耗时及请求错误率,和接口质量以及用户网络状况强相关,所以此次性能优化的重点放在启动性能以及运行性能。
2. 问题识别
2.1 “碎片化”优化
“碎片化”主要指优化动作过于离散,不连贯无法形成体系。
过去在启动速度不达标时,也会按照小程序平台开发者工具建议优化,额外也有些分包、缩包、图片压缩等动作,但有局限性无法形成合力,依旧存在无法解决的问题:
优化效果有限,无法达到「优秀」官方评测结果;
启动速度在需求迭代中反弹;
过于定制化无法陈定并固化至后续需求研发中;
2.2 性能指标难保持
增加启动阶段的理解、利用好开发者工具、借鉴官方启动优化实践,通过常规动作大概率可以实现一段时间启动性能指标的下降。但要实现指标长期稳定,依旧存在问题:
指标监控体系缺失:小程序后台虽然会提供性能数据,但数据散落在各个平台,很难在业务迭代中持续关注并跟进。另外一旦指标波动,用户第一时间就会感知,体验下降,而研发是“后知后觉”的;
业务需求迭代包体积增加:过去3个季度,达达快送小程序的发单场景由 2个 -> 8个;
碎片化:“碎片化”的优化实践,不成体系无法系统性解决性能问题;
团队成员变动:相关文档少,人员变动时无法有效沉淀;
......
2.3 小程序性能分析
本地开发中微信小程序提供了体验评分工具它会在小程序运行过程中实时检查,分析出一些可能导致体验不好的地方,并且定位出哪里有问题,以及给出一些优化建议。支付宝小程序可以使用全息检测。
除了本地开发我们可以参考官方后台,评分工具是在本地运行小程序代码时进行分析,但性能数据往往需要在真实环境和大数据量下才更有说服力。恰巧,小程序管理平台 开发者提供了大量的真实数据统计。
3. 关键路径
虽然不同平台对小程序启动耗时的各有一套定义,但往往也只是对启动耗时终点定义不一致,小程序的启动流程非常类似,大致会包含以下四步:
小程序相关信息准备
框架资源准备
业务资源准备
加载渲染
以微信小程序为例,启动耗时指标统计的终点是生命周期onReady作为时间的结束,比较趋近于FCP(First Contentful Paint)指标。而支付宝小程序则更加趋近于TTI(Time to Interactive)指标。
FCP:首次绘制任何文本,图像,非空白canvas或SVG的时间点;
TTI:度量从页面开始加载到主要的子资源加载完成并且能够快速可靠地响应用户交互的时间;
启动性能指标是个综合结果体现,不同阶段会用户、平台以及小程序本身的影响。因此我们以支付宝平台提供的全息检测工具,梳理了各个细分阶段影响因素。
阶段定义:
setUp:应用初始化,主要是db查询时间;
Update:查询更新信息,对比差异;
offine: 下载离线包;
waitLoadApp:解压,等待渲染
分析各个阶段影响因素确定四个核心优化思路:
静态资源优化,控制总包、主包、分包大小;
接口聚合,合并接口并尽可能串行改并行,缩短最短请求路径;
启动页模块调整,分析启动页功能模块,在不影响业务流程前提下,优化交互方式,提升页面渲染性能;
沉淀传承,总结归纳优秀实践,持续保持优秀性能指标
3.1 静态资源优化
如上文所述,资源包体积对小程序包下载、包解压、代码注入等的多个启动阶段都有不同程度的影响。而且小程序平台对包体积大小也有明确限制,以微信平台为例:
整个小程序所有分包大小不超过 20M;
单个分包/主包大小不能超过 2M。
所以资源包体积大小对启动性能也尤为重要,因此选择【静态资源优化】作为第一步优化动作。
3.2.1 包体积优化
核心思路:从包加载时机和包体积大小两个维度思考。对应动作分别是:预加载、缩包、分包异步化。
预加载:主要指微信小程序平台,通过平台提供的预拉取能力,在不影响主包的情况下将分包加载时机提前,提升分包页面的加载速度。
缩包
分包:将相对独立的页面和组件拆分到分包,可以解决主包体积受限问题。但随着业务的迭代实际开发中会遇到主包即使页面很少也会过大,经过长期的实践沉淀出一套适合我们的分包策略。
无用文件、函数、样式剔除:业务迭代无法避免会产生无用文件,小程序可以入"lazyCodeLoading":"requiredComponents"(在开启「按需注入」特性的前提下,「用时注入」可以指定一部分自定义组件不在小程序启动时注入,而是在真正渲染的时候才进行注入),我们也可以用工具分析没有用到的文件和依赖,进行删除
延迟加载:分包一定程度可以缩减主包体积,但也遇到:
插件无法实现分包处理
多业务的分包难以划分
所以我们采用了分包异步化(该能力目前只在微信小程序中有)使用componentPlaceholder占位,异步拉取分包加载,完成分包下载和注入后,将占位组件替换成真正的组件。
3.2.2 图片资源处理
压缩图片:上传的图片可以使用二倍图(当设备像素比很大时,图片会被放大,而放大会让图片看起来模糊。为此,我们可以使用二倍图的方式来提高图片的清晰度)并且压缩,大图片不要超过100KB。利用一些压缩技术对图片进行压缩推荐 ,压缩尺寸可观,但对图片显示质量影响甚微。
小图片使用IconFont:避免小图片作为CDN的情况,因为HTTP一个域名下有请求并发限制,过多图片会造成队头阻塞。可以使用IconFont和base64等方式。
大图片统一使用CDN:在图片处理中,因为我们遇到情况是大部分都是运营配置的图片,可以在配置的后台限制大小,自动转化成webp格式(webp它可让网页图档有效进行压缩,同时又不影响图片格式兼容与实际清晰度,进而让整体网页下载速度加快)。
懒加载图片:屏幕外的图片可以使用懒加载,只有下拉之后才去加载对应的图片。
3.2 接口聚合&&时序优化
在小程序中,发起网络请求是利用wx.request来发送的,不同于浏览器一个域名并发的限制,wx.request调用的并发限制为10。超出限制的请求会被阻塞。所以针对接口我们主要采取的举措有:
实时性较低的接口缓存结果:可以采用的策略是,频繁使用的数据缓存到内存,实时性较低的结果缓存到本地,下次启动可以读取到内存中继续使用,也可以给缓存设置一个时效,更新本地缓存
减少接口串行:接口没有依赖关系的可以考虑使用Promise.all
预拉取首屏资源:预拉取能够在小程序冷启动的时候提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间
接口合并,优化速度:分析接口询问后端是否可以合并接口,优化稍慢的接口
小结:在分析接口层面时,我们整理了小程序首页的接口调用的时序图:
优化前: 总共用户进入小程序后 21个(埋点接口和个端差异接口未计算在内)
优化后: 总共用户进入小程序后 17个(埋点接口和个端差异接口未计算在内)
对比优化的结果由原来的串行长度为10->6,接口总数减少4个,并加入了接口缓存,预拉取,本次优化在支付宝大约带来了500ms的下降。
3.3 启动页模块调整
不管是微信还是支付宝小程序启动速度的考量并不单单是首页的耗时,包含所有入口页面。达达小程序有查看订单详情,活动页等都会作为启动页投放。我们对业务梳理针对性的做了优化:
首页
首页预先拉取了金刚位,banner,广告位等资源,因为实际调用中获取这些资源链路比较长,我们冷启动中获取资源在小程序启动直接渲染,在真实调用中去比较数据的变化,增量更新,虽然首页是千人千面的页面,但是绝大多数内容是类似的,提前渲染可以提升总体加载时间;
对于获取位置信息是比较耗时的,我们在用户授权后缓存了结果到内存,并且存入本地设置了一个时效,在用户下次打开时,未过期的情况下可以直接读取本地I/O。考虑到位置信息需要调用多个接口获取且较慢,所以提升较大;
在分析包大小时发现主包存在过大的SDK,但没有办法去除,或者利用plugin方式加载。最后是将涉及插件的SDK拆到分包中利用分包异步化去加载;
首页弹窗逻辑之前有5种+,通过让后台承担更多的业务逻辑,可以节省小程序前端代码量,以前的做法是前端从不同接口获取资源,然后通过优先级去展示,前端做很多逻辑处理,现在有后端实现了一套广告位系统,减少了接口调用和逻辑判断。
物流详情页
针对天降红包,因为他串行3个接口,影响较大。我们执行了发券用户操作之后去加载红包;
针对地图的展示,去除了非必要的路径规划,折叠展示了地图
本次业务流程的优化,在支付宝小程序总体降低了400ms
3.4 优秀实践
“突击优化”一定程度上能够提升性能指标,但持续保持性能指标的稳定则功在平时。因此除了固化一些专项优化策略,比如:分包策略、图片处理策略等,还需要沉淀更多实践并传承。
流程类:
建立指标监控体系:主要包含:包大小、图片大小、启动耗时、评测结果。过去研发对于指标的波动是“后知后觉”的,指标监控利于研发尽早发现问题,减少对用户影响。
规范发版:除了代码包体积代码注入、首屏渲染之外,发版频率等因素也会影响小程序启动耗时,通过与业务方确认在不影响小程序正常功能迭代的前提下,约定每周2次固定发版。
代码类:
要提升渲染性能就不得不了解小程序的架构:
可以从图中看出,由于渲染层与逻辑层分开,一个小程序有多个界面,所以渲染层对应存在多webview。这两个线程之间由Native层进行统一处理。无论是线程之间的通讯、数据的传递、网络请求都由Native层做转发。所以如何减少数据交互的频率和大小是提升渲染性能的关键。
代码类:
合并setData调用:尽可能合并setData的调用,将多次setData合并为一次,可以模仿vue的nextTick写合并的逻辑(调用 setState 时提供的对象会被加入到一个数组中,当下一次事件循环执行的时候再把这些对象合并一起,通过 setData 传递给原生小程序),支付宝小程序可以调用batchedUpdates来批量更新,减少调用次数;
减少setData大小:传输的数据量越多,线程间通信的耗时越长,渲染速度就越慢,我们的做法是将不设计渲染的数据放入_data中;
去掉不必要的属性节点,绑定属性数据大小:在xml中绑定属性data-xxx,尽量减少数据的大小,视图层会把事件 target 和 dataset 数据传输给逻辑层。那么,当自定义数据量越大,事件通信的耗时就会越长;
JSAPI调用限制:一些同步API会阻塞渲染,启动页面减少使用 Sync 结尾的同步 API ,getSystemInfoSync, getStorageSync,setStorageSync。可以缓存结果,或者使用异步获取数据。原则就是:减少使用,缓存结果。
依赖库优化:删除了埋点框架中一些非必要的同步API的获取操作,或者延后执行,将频繁调用的API结果缓存到内存中,减少调用
......
4. 总结&规划
我们来梳理一下整体的优化思路,如下图所示:
4.1 成果
微信平台
启动耗时降低20%;
过去两个季度,启动耗时长期维持在优秀;
打开率提升0.5%
支付宝平台
C端支付宝小程序 【达达快送】启动耗时降低40%,现在维持在优秀水平。
服务竞争力指数 SCI (支付宝平台):提升至满分。
4.2 规划
经过本次优化微信和支付宝小程序都达到了官方的优秀指标,优化的经验适用所有小程序。但如何监控启动速度,如何结合自身业务定义小程序性能指标,出现问题及时告警,能够定位具体的原因,需要建立一套自己监控体系,持续保持小程序性能。