小程序加载性能优化实践

1,112

前言

对于互联网产品来说,第一印象就是应用的启动速度。虽然启动足够快时用户不会有很大的感知,但是如果慢就会被发现就会被挑战,总结来说,快就是应该的。

而应用的启动速度优化,又可以分成首次启动速度优化和二次启动速度优化。对于不同的类型,对应的优化方案也是截然不同的。要如何确定优化方向以及优先级呢,这就要从具体的业务场景出发。

业务场景

但凡不谈业务场景就直接谈优化都是不够专业的。

本次优化实践主要是依托在微保的车险业务。

众所周知,车险一般都是一年期的保险。因此车险用户的访问频次非常低,一年一次。其次,因监管要求,用户在投保车险之前,必须要先完成实名认证;认证通过之后才能添加要投保的车辆。因此一个用户要想完成投保,一定要经过实名认证、添加车辆、报价、支付、完成投保等步骤。

另外,为了优化用户访问频次低的问题,平台通常会提供一些车主必备服务。以提升用户活跃度。

由此可见,微保车险的业务矩阵大致如下:

相较于车主服务,主流程的性能显得更为重要,毕竟涉及到成交的流程才是关键。因此,本次总结也是在 主流程 上优化实践得出的。

优化方向

绝大多数车主用户只拥有一辆汽车。因此一年之中就只有一次购买车险的机会。对于如此低频的产品,每次用户过来,都是全新的UI,全新的代码。相当于每次过来都是首次启动,此时 首次启动速度 就显得极为重要。这也是本次优化的主要方向。

我们参考Google提出的RAIL性能模型目标。

  • 以用户为中心;最终目标不是让您的网站在任何特定设备上都能运行很快,而是使大部分用户满意。
  • 立即响应用户;在 100 毫秒以内确认用户输入。
  • 设置动画或滚动时,在 10 毫秒以内生成帧。
  • 最大程度增加主线程的空闲时间。
  • 持续吸引用户;在 1000 毫秒以内呈现交互内容。

总结起来,我们的第一优化原则就是,保证绝大多数的用户能够较快地访问我们的应用。

准备工作

展开性能优化工作之前,还有一个重要的准备工作。即要预先采集基准数据。有了基准数据,才能很好地衡量自己的优化效果。

微信公众平台虽有提供小程序启动耗时、下载耗时的图表,但由于不能提供原始数据,不利于细化分析,因此本次性能优化采用的是自建日志上报系统提供的数据。

确定好性能指标,获取到相关数据,这还不够。

由于现实网络环境非常复杂,收集的原始数据是非常粗犷的,相同的代码,相同的配置,在不同的网络环境和手机上,加载时间会千差万别。因此对于收集到各种极端数据,必须要做一定的加工处理,才能变成可分析的数据:

常见的加工处理(指标计算口径)主要如下几种:

  • 平均数: 最直接的想法,计算所有数据的平均值。但是平均数会因为部分极端值导致偏大,会观察到数据的波动较大,因此平均数不适合作为参考指标。
  • 95分位数: 将所有数据从小到大排列,取第 95%位置的数值,将这个值作为我们的性能指标。
  • 截尾平均数: 同样将所有数据顺序排列,将尾部的部分数据移除,然后取平均值。这个值会相对平稳,比较适合作为参考指标。

结合前面提到的第一优化原则,明显 截尾平均数 这种指标计算口径是最佳选择。

比如假设我们承诺保证90%的用户在1000ms内完成小程序的加载,那么我们就只要关注前90%的用户的加载耗时是否在1000ms内即可。

通过截尾获得耗时较少的前50%、80%、90%用户的平均加载耗时数据(后续横向分析这三类用户的加载耗时的优化效果)

处理好数据之后,进行性能分析的时,可以如下进行分类分析:

  • 按手机系统(Android、iOS)分类
  • 按网络环境(WIFI和4G)分类

以下是采集的 4G网络下,不区分手机系统 的车险分包的加载耗时(作为优化前的基线数据):

实践

由于小程序有提供许多基础API、UI库,而这部分代码是每个小程序的依赖。因此初始化代码的时候,这个是最先初始化的,渲染流程大致如下:

相对于传统web,我们能在很多环节上做优化。比如资源加载顺序(图片懒加载、CSS前置加载,JS后置加载等),使用构建工具实现按需加载等。但小程序的底层逻辑比较封闭,我们无法深入参与,因此要优化小程序的加载性能,能做且最行之有效的方案就是优化小程序代码包的大小。

优化代码包大小的方案主要有以下几种:

  • 移除未使用的代码
  • 实现代码按需加载(在小程序里,即是采用分包加载方案)
  • 移除小程序包里的图片资源(因为小程序包下载时,默认使用了gzip压缩,而非文本的压缩效率较低)
  • 简化JavaScript,尽量避免在前端进行复杂的计算(将JS逻辑尽量迁移至后端或者采用Nodejs中台来完成这部分计算工作,这样也利于后续扩展到多平台)

移除未使用的代码

由于车险是微保最先推出的产品,经历了无数多个迭代,不可避免会出现已下线的业务或功能。这部分代码一般都是以页面为单位,因此可以结合PV数据来找到这部分代码。

提到PV数据,顺带提一下如何自建日志上报系统。由于小程序的每个页面都有完整的生命周期,因此进入一个新页面时,都会触发Page的onLoad方法,此时通过向后台服务上报进入页面的信息,即可完成页面PV数据统计。

对于如何实现错误路径的重定向处理,提供两个思路:

  1. 小程序提供的路由能力(如wx.navigateTo),当传入的url不存在时,会触发 fail 方法,此时在 fail 处理下即可。
  2. 使用 wx.onPageNotFound 方法处理,不过有一定的兼容性问题。

移除未使用的代码之后,车险分包由 1100+KB 降到 900+KB (减小18%)

代码按需加载

理想情况下,访问用户应该只下载有访问到的资源,其他资源一概等到需要的时候才下载。这种按需加载的方案,小程序是采用代码分包的方式处理。但由于分包与分包之间无法相互引用资源,无法复用代码,因此拆分分包必然导致开发的效率下降。因此拆分分包时,要把握好性能与效率的平衡。

由此,我们提出了 基于UV及访问路径的分包拆分原则

结合日志系统,我们发现车险的实名认证页是UV大户,而实名认证的逻辑和添加车辆、编辑车辆的逻辑比较类似,均包含了车辆管理逻辑和组件。因此这部分页面的代码相似度较高,比较适合拆成一个分包,最终将实名认证、车辆管理等页面拆分到:新用户分包A。

其次,通过观察页面漏斗数据发现,大部分的用户只访问了主流程上的页面:车险首页、报价页、支付页、保单详情页。至于调整方案页、车辆列表页等分支流程,只有少部分用户会访问到的。因此将主流程的页面拆分成主分包B,而其他所有分支流程的页面则拆分成另外一个分包:其他分包C。

最后将车险的分包拆分成如下:

  • 包含实名认证、车辆管理流程的新用户分包A(200+KB)
  • 包含主流程的主分包B(400+KB)
  • 包含分支流程的其他分包C(100+KB)

对于新用户(未完成实名认证),通过预检测实名情况,分发不同路径,从而实现新用户仅先加载新用户分包A,与此同时预加载主分包B。因此对于新用户来说,首次加载的是200+KB的新用户分包A,相对于优化前加载的900+KB完整分包,加载的小程序分包大小减少了78%。

对于旧用户(已完成实名认证),不再加载新用户分包A,与此同时预加载了其他分包C,因此加载的小程序分包大小减少了56%。

简化JavaScript

这部分优化与传统web开发的优化原理类似。

代码包下载完成之后,就要完成业务代码注入。这时,JS引擎就要解析/编译JavaScript,这也是JS引擎最耗时的操作,从Chrome开发者工具可以看到,其中黄色部分就是解析/编译耗时的部分:

因历史遗留问题,导致一些复杂的数据需要前端拼凑计算处理(如可续保车辆的计算等)。将这部分逻辑迁移至后端后,JavaScript的代码得到了进一步简化,只对最终数据做基本展示变换。

由于本次实践没有深入这方面的优化,故不详细叙述这块优化的效果,后续有机会另外开篇文章详细讲。

遇到的问题

在本次优化实践的过程中,遇到了许多问题,和大家分享一下我们的处理办法。

第一个遇到的问题,就是组件的引用。

因为小程序的限制,组件不能跨分包互相引用,要想跨分包复用,就要将组件放在小程序主包,而主包的代码是跨业务共有的,不能随意添加。因此在拆分分包的时候,需考虑到这个问题。

其次,是JS公共库的引用问题。

原本在同一个分包时,通过相对路径引入即可。但拆分成多个分包之后,JS就无法直接引用了。此时需要变换思路,如:将JS公共库挂载到全局变量global上;或者将代码copy一份到其他分包上(当然不是直接copy,是通过gulp或者webpack打包);又或者将公共库放在主包上。

还有更让人头疼的是,目录变更导致的路由跳转问题。

由于小程序分包机制要求每个分包都在一个目录里。因此拆分分包就不可避免地要对文件进行迁移,而小程序的跳转又和文件目录强耦合,文件路径变更导致跳转路径变化。因此涉及到的页面的跳转路径均要改变。

此时,还要考虑向前兼容,比如已推送的模板通知。因此路由跳转的封装就显得极为必要。

效果

经历以上的抽茧剥丝,终于等到了上线时间,可以到看,优化的效果非常明显:

横向对比不同分类的用户,可以更清晰看到各类用户的提升情况:

对于较快的前50%用户,也提升了接近50%的访问速度。

此次优化,保证了90%的用户可以在2000ms内完成小程序的加载。

后续优化

对于接下来的优化工作,我们仍有几个计划:

充分利用小程序提供的预下载分包功能。若没有设置预下载,用户在跳转其他分包页面时,就要等待分包的下载。对于比较急躁的用户,会以为卡机了,最终导致用户跳出。(小程序预下载的唯一限制就是,当前分包和预下载的分包大小之和不能超出2M,如果超出,可以考虑再次拆分分包)

简化WXML的class。类选择器是最常见的样式选择器,当页面变得庞大时,class的长度往往会越来越长。然而在不支持DOM操作的小程序里,class的主要作用主要是样式注入。因此可以通过脚本将WXML和WXSS相同的class统一精简成短小的命名。

采用Node.js中台转移部分计算。将JavaScript的代码进一步简化,这样有利于对小程序的加载性能进一步优化。其次,这样也有利用扩展到其他平台,比如H5等。

总结

尽管看似微信小程序的开发、发布方式和传统web有很大的差异。但是底层的运行还是类似的,因此可以从传统的web优化实践中找到优化的思路。

其中本文提到的一些优化实践是非常定制化的,是根据特殊的业务场景采取的特殊方式处理。毫无疑问,业务在发展,代码也会随着发生变动,这就需要不断的优化,才让用户的体验愈来愈好。

感谢大家阅读,如有错误,欢迎指正,感谢。