腾讯云医小程序性能优化与监控的实战转载
前言
腾讯云医业务是为医生与患者打造的云上一站式医疗服务平台,为了降低医生首次入驻平台的成本;利用好微信的私域流量;保证医患双方有更接近端原生的使用体验,我们选择以微信小程序作为医患双端的核心承载形式。
小程序相对于原生本来就存在一定的性能差距,云医小程序上线后,随着入驻医生,使用患者的稳步增加,服务场景的不断丰富,微信小程序的性能开始受到了越来愈多的挑战。
在当下阶段,云医小程序的性能表现直接影响到用户的长期留存,因此,我们对腾讯云医医患双端进行了一次全方位的性能优化,从加载、渲染等维度深挖小程序的性能可塑性,提升腾讯云医总体的使用体验。
当前问题
在当前,我们面临的最严峻的问题有两个:
1、加载时间慢:云医首页作为核心入口,承载了运营位、资讯广场、医生个人资料、常用功能入口等多项功能。间接导致首屏接口请求多,加载时间慢。整个小程序的首次冷启时间超过5s。而加载时长又直接影响到小程序的到达率。根据《High performance iOS Apps》中用研结论,25% 的用户在应用启动时间超过 3s 时会放弃使用。
2、运行时体验差:以高频场景——诊室会话页为例,会话页面临长列表页面卡顿,滚动不流畅,随着历史消息加载,小程序闪退等问题。这些问题带来了极低的用户任务完成率,无法满足用户诉求,拉低用户留存。
优化历程
在整个性能优化的历程中,我们采用了一个递进的优化策略:
1、监控:发现问题在哪 ———— 性能优化的前提是性能监控,通过监控我们才可以定位性能瓶颈。
2、优化:怎么解决这些问题 ———— 这一步,我们总结了一套通用的性能优化方案。
3、保障:问题怎么不再出现 ———— 如果在业务的快速迭代中做好高性能的保障,是我们需要长期克服的难题。
一、打造小程序性能监控体系
首先说监控,在开始做小程序性能监控的时候,我们面临了两个难点:
微信官方没有给出权威的性能指标建议,如何将之前提到的性能问题转化为具体可量化的指标?
指标定好后,如何设计并落地一套稳定、高效、对性能有指导意义的监控方案?
下面说一说我们是怎么解决这难点的:
难点一:性能指标定义
当我们说到合理的性能指标的时候,我觉得这个问题可以换一种方式来问:
性能优化指标如何与用户体验优化的指标结合起来?
按照我们的优化目标,以及小程序官方建议,我们将小程序的性能分为了 启动性能 和 运行时性能,我认为它们和用户体验可以组成这样的一一对应关系:
启动性能 ————用户进入小程序时的加载速度。如果白屏时间过长,超出用户忍耐限度,则用户会选择放弃。
运行时性能 ———— 性能不止是 “足够快”;除了首次加载速度以外,我们还需要关注到小程序使用过程是否卡顿、是否常常会闪退、是否有白屏过程,是否会出现用户操作无响应等;全方位衡量用户在使用过程中能感知到的每个节点。
启动性能
为了得到启动性能指标,我们从 小程序框架 还有 用户感知 两个维度分析了小程序启动全流程:
整个小程序的启动流程大概分为了五步:
1、包拉取阶段:此时用户会看到一个 loading,这个时期框架做的主要事情包括:
- 信息准备:微信客户端需要从微信后台获取小程序的配置、版本、权限等相关信息,以对小程序进行必要的版本管理、权限控制和校验。
- 运行环境准备:在执行小程序代码之前,微信客户端需要准备小程序运行的基础环境。
- 拉小程序代码包:小程序托管在腾讯云上,一个小程序可以由一个主包和若干分包组成,小程序启动时需要从服务器获取代码包地址、下载小程序代码包。
2、代码注入阶段:小程序分为逻辑层和渲染层,在这个阶段,框架会从代码包内读取小程序的配置和代码,将 js 代码注入到 JS 引擎中,并触发小程序的 App.onLaunch 生命周期,我们将 App.onLaunch 视作小程序启动完成的标志。wxss 和 wxml 会经过编译注入到渲染层, 包含页面渲染需要的页面结构和样式信息。渲染层的注入耗时主要和页面结构复杂度和使用的自定义组件数量有关。
3、首屏创建阶段:这个时期逻辑层会创建页面实例,渲染层等待 initialData 进行渲染。
4、首屏首次渲染阶段:结合逻辑层得到的初始数据和渲染层得到的页面结构和样式信息,框架会进行小程序首屏的渲染,并在渲染完成后触发首屏的 Page.onReady 事件。这个时候用户会看到一个基于前端默认的数据状态的页面,由于这不是后端接口请求返回的真实数据,可以认为用户此时看到的信息是没有有价值的。
5、首屏可交互阶段:如果 onLoad 和 onShow 中没有其他方法,那么初次渲染完成后,页面已经是一个可交互的状态了。但实际情况下,页面中往往有很多逻辑方法和异步接口请求,请求到具体业务数据完成计算之后,进行多次 setData,并渲染最终的页面。这个时候用户才会看到完整的首屏内容。
到这里为止,我们就简单的了解下启动阶段小程序做了什么事情,可以看到这个过程中,从开发者的角度,可以影响小程序启动流程的步骤有以下几个:
1、主/分包的下载和载入时间。
2、代码注入时间
3、onLaunch 和 onShow 同步脚本的执行时间
4、首屏首次渲染时间
5、首屏核心区域可交互时间
我们把这几个步骤对应到五个启动性能指标,再加一个启动总耗时,此时,启动性能的指标就制定好了。
运行时性能
接着再说一下运行时性能,我们针对用户在运行过程中遇到性能问题进行分析,并把它们抽象为三类:体验类、异常类、加载类。
- 体验类:表现主要是页面卡顿。
- 异常类:表现为白屏或者小程序闪退。
- 加载类:页面首次加载或者某一个模块下载慢。
然后针对这些性能问题的表现,总结原因,再将它们对应成立相应的运行时性能指标,此时,运行时性能指标就制定好了:
这里重点解释一下 setData 耗时、运行内存告警:
setData 耗时:setData 是小程序在进行数据更新的时候调用的跨线程通信 API, 无论是用户交互还是后端接口返回数据,都会发生数据更新,setData 耗时的起始点是在逻辑层发起调用时,终止点是渲染层完成渲染并通知到逻辑层的时候,所以 setData 耗时可以最直观的反映出页面的渲染情况。
内存告警:小程序闪退一般是由于运行内存不足引起的;当小程序占用系统资源过高,可能会被系统销毁或被微信客户端主动回收,由于我们无法直接监控到小程序闪退,所以通过监控内存告警的方式来间接关注,分析出哪些页面 Crash 率比较高,从而针对性地做优化。
到此为止,指标定义的过程就讲完了。
难点二:监控方案的选择与落地
在做监控方案之前,我们定了三个目标:
- 指标收集:监控手段要能帮助我们收集到上面总结的性能指标,这也是最基本的要求。
- 明细数据:我们不仅要拿到性能指标本身,我们还要拿到很多附加信息,例如当前页面、用户机型、网络情况、这样才可以对数据进一步维度拆分、才能对性能优化有实际的指导意义。
- 灵活度:我们希望可以自己管控数据存储,一方面是为了把控数据存续时间,另一方面是为了方便做功能拓展,例如数据分析及告警。
基于这样三个目标,我们选择了自定义上报的方式来做性能监控,这是初版的性能监控方案:
主要采用无埋点上报的思路。通过劫持微信的生命周期方法和部分微信的 api 计算耗时,它的优点是对业务代码几乎零侵入,对业务逻辑影响小,可移植性强。
但是,这个时候我们面临了一个问题,基于自定义上报的监控方案在指标收集上还有缺失。
为了解决这个问题,我们针对微信官方和公司的性能监控手段进行了分析来补充它缺失的能力:
并在最后选用了ppt里的绿色部分作为我们的性能监控手段的补充:
它们分别是:
- 性能 Trace 工具 / 性能面板:在开发版小程序下可以打开性能面板,看到小程序运行中的部分性能数据,通过它,我们可以得到 “运行内存” 这种常规前端监控手段得不到的数据。
- 微信开放平台指标:微信官方提供的服务端调用接口,与小程序后台比起来自由度更高,可以由业务方自己做数据的存储和展示。
- wx.getperfotmance:前端调用的 API ,我们利用它,获得我们关注的启动总耗时 ,代码注入时间。
- TAM :公司级一站式前端监控解决方案,它偏重传统web前端监控能力,我们利用它来补全前端错误监控。
- miniprogram-ci:从微信开发者工具中抽离的关于小程序代码编译模块,可以通过它在每次流水线的时候获得主包,分包的大小。
就这样,我们构建好了自己的性能监控体系。他的主流程是在小程序运行过程中,通过自动上报向微信官方上报数据,通过 wx.getperformance 和自定义上报向 TAM 上报数据,然后分别从 微信开放平台以及 TAM 开放平台进行数据拉取,数据处理,数据入库和展示。
这个时候,我们又面临了第二个问题:怎么保证服务的稳定性和数据的完整性,为了解决这个问题,我们做了以下几件事情:
- 增加再次拉取机制保证数据拉取成功率,5点 ,20点分别拉取昨日数据。
- 数据拉取重复性校验,对已经拉取成功的数据,再次拉取时直接跳过拉取。
- 增加人为干预机制,开放 API,在有数据缺失或者数据异常的时候,可以手动触发的方式进行补充。
- 增加基于 json-schema 数据自检能力,针对数据合法性做异步校验,有问题的数据剔除并报警。
到此为止,整个小程序性能监控体系就构建好了。
二、性能优化
监控的部分讲完了,下面说一下优化的部分。在监控体系构建好之后,我们就可以把性能问题以指标形式量化了(iOS 13 / 4G / iPhone 11)。
以下是优化前云医的性能报道,我们通过和微信体验评分以及业内性能表现优秀的小程序做对比,找到以下性能瓶颈,主要是表格的中红字部分。
针对这些指标做一个简单的分析,可以看到问题主要有三个:
(一)
在小程序启动过程中,appLaunch 时间平均有 2s 左右,首屏核心区域可交互时间平均有 2 到 3 s,这两个时间都太长了。
(二)
类似于上图中这种 IM 场景的长列表页面,在拉取列表数据过程中,setData 时间随着数据拉取不断的增大,导致页面越来越卡顿。并且运行内存也有增大的趋势,刚进入页面的时候运行内存大概在 100M 左右,随着使用过程可能会逐渐上升到 800M.
因此,我们确立了三个重点的优化方向,分别是启动优化,首屏渲染优化,setData 优化.
重点一:启动时间(appLaunch)优化
首先讲启动(appLaunch)时间优化,对启动初始化时间进行简单分析,前面这一部分信息/环境准备时间作为开发者而言是没有优化空间的,因此我们主要做的事情就是包拉取耗时以及代码注入耗时的优化。
启动时间(appLaunch) - 包拉取耗时优化
包拉取耗时与小程序主包大小正相关,经过简单的调研我们发现,当整个小程序的主包体积保持在 1M 以内的时候, 拉包时间大约可以控制在 1s 左右。我们当时的主包大小已经到 1.9M,即将达到微信的主包上限 2M,因此,我们决定针对小程序包体积进行一次瘦身。
在做包体积优化前,需要考虑的主要问题有两个:
- 优化包体积的时候不可避免要删掉很多模块,如何防止误删影响线上功能 ?
- 小程序里资源类型较多,如何针对每一种资源找到适合它的优化策略?
问题一的解决策略是:通过微信开发工具静态分析功能找出无依赖模块;通过蓝盾流水线接入code CC 插件找出重复模块。
这样就可以快速把要移除的模块快速找出来,并且对线上功能影响最小。在优化完之后,我们也进行功能全量测试再上线。避免对线上产生不良影响。
问题二的解决策略是:使用传统 web 场景优化手段针对文件资源进行优化;使用小程序场景特殊优化手段针对分包,组件进行优化。下面这张图包含了我们用到的所有包大小优化策略:
它的核心思路可以用四个字概括:
- 删:已下线、已废弃、无关、冗余不再需要的内容进行删除。
- 搬:将所有非核心非必要的内容搬出主包。静态资源可以迁到 cdn,静态页面可以挪至 h5,通过 webview 载入,非 tab 页用到的公共组件以及页面也尽量挪到分包。
- 压:针对不同的资源类型使用不同的压缩方式,例如通过 js-treeshaking 对 vendor.js 进行压缩,把 png 改为 jpg 格式体积可以减少 50%等等。
- 合:将可以复用的模块进行合并。
经过一系列优化后,医生端的代码包从 1.9M 缩小到 1.36M;患者端的代码包从 1.57M 缩小到 1M, 医生端启动耗时大概下降了 300ms 左右。
启动时间(appLaunch) - 代码注入耗时优化
在小程序启动时,主包里的代码会统一注入到小程序运行环境,打包成一个 appjs (一般都有 1-2 M),这里面包括了首屏用不到的逻辑代码,影响启动耗时。
解决方案:开启了小程序官方提供的代码懒注入,原理类似 webpack 按需打包,仅注入当前页面需要的自定义组件和当前页面代码。开发者可以在 app.json 中配置:
{
"lazyCodeLoading": "requiredComponents"
}
开启这项配置之后,代码注入的时间下降了 50%,启动耗时大概减少 150ms 左右。
Tips: 官方提供的 useExtendedLib 扩展库 api 和 lazyCodeLoading 同时使用会存在冲突暂未解决,因此用到 useExtendedLib 的情况下无法使用 lazyCodeLoading。
启动时间(appLaunch)优化 - 总收益
针对医患双端进行优化完后,小程序启动( applaunch )时间大概 下降20% 左右
重点二:首屏渲染优化
下面说一下重点二,首屏渲染优化
首先我们针对首屏渲染时间进行详细分析,在小程序启动完成后,会发起两个请求,分别是登陆接口和获取医生信息的接口,首页所有其他的逻辑要等待这两个接口请求完毕,严重阻塞了首屏时间;
另外,首页其他的接口和渲染耗时加起来也有1900ms,因此,针对这两部分用到的优化策略分别是首屏阻塞时间优化以及首页接口逻辑优化。
首屏渲染 - 首屏阻塞时间优化
登陆接口平均耗时有600ms,我们首先针对这部分进行分析,发现建立 SSL 连接的时间就占了整个接口的50%,那么这部分有没有办法优化呢?经详细调研后发现,这个过程中需要和微信多次交互,没有优化空间。我们还想到一个办法,针对这两个接口在后端进行聚合,但是聚合完后也只优化了 100ms,这个对于首屏而言是远远不够的。
最终解决方式是采用数据预拉取,也就是在小程序启动时,通过微信的服务器代理小程序发起一个 HTTP 请求到第三方服务器来获取数据,并且把响应数据存储在本地客户端供小程序前端调取。
当小程序加载完成后,只需调用微信提供的 API wx.getBackgroundFetchData 从本地缓存获取数据。通过这种方式,我们可以把这两个阻塞的请求发起的时间从 appLaunch 的时间提前到用户点击进入小程序的时间。
预拉取上线后,首屏核心区域可交互时间大概减少了 700ms,优化效果非常明显。
tips:预拉取的数据会被强缓存,在缓存失效前微信客户端不会再次发起请求,所以对于数据实时性要求较高的接口不适用于使用预拉取。
首屏渲染 - 首页接口逻辑优化
医生端首页作为核心入口,承载功能较多。首屏共调用了 18 个业务接口,还有自己的日志上报和云通信查询。
接口堆积排队导致加载较慢,同时我们还发现首页实现逻辑上还有一些不合理的地方,比如一些不需要频繁更新的状态查询放在了 Page.show里面。
针对这部分,我们主要用到的优化方式是:
1、关键请求提前,例如获取医生注册信息,从首页移至 app.onLaunch。从 app.onLaunch 到首页的 onLoad 大概有 200ms ~ 300ms,这就赢得了 200ms 的首屏渲染时间。
2、非关键请求延后,例如非第一屏的数据,放到最后请求。
3、将一些更新较频繁,但是频率接近的接口,在后端做了接口聚合。
4、没有上下依赖关系的串行请求,改为并行。
5、优化了静态资源的缓存逻辑,由于我们的静态资源都使用了 hash,不存在同名不同文件问题,我们将静态资源的浏览器缓存时间从默认的十分钟延长至一周。
6、开启 http2:wx.request 从 2.10.4 开始支持 http2,我们知道:在 HTTP/1.1 中,如果客户端想发送多个并行的请求,那么必须使用多个 TCP 连接,而 HTTP/2 的二进制分帧层突破了这一限制,所有的请求和响应都在同一个 TCP 连接上发送。这样就可以消除不必要的延迟,从而减少页面加载的时间。
除了上述优化以外,我们还用到了接口缓存,我们发现云医首屏大部分接口数据比较稳定,时效性要求低,因此我们设计了一套通用的接口数据缓存方案,接口请求数据先读取本地缓存数据。
无论本地缓存数据有没有都发送 http 请求。我们本地缓存的数据只是作为页面的首次渲染数据,减少用户等待时间。接口成功返回后更新本地缓存,再用新数据更新页面,这样确保用户看到最新的数据。
使用首屏接口缓存后,页面核心区域可交互时间大概下降了 300ms 左右
首屏渲染 - 总收益
首屏渲染优化完成后,医生端首屏核心区域可交互时间 从 2560ms 减少至 983ms,可交互时间缩短了一倍多。患者端首屏核心区域可交互时间从 1030ms 减少至 650ms
重点三:setData 优化
当前我们面临的问题是在长列表页面,setData 时长不断增大,导致页面卡顿。首先针对这个问题进行一个简单的分析:
1、setData 是小程序内用于数据更新的跨线程通信方式,本身就是一个比较昂贵的操作,在长列表页面,setdata 数据量大,调用次数频繁,进一步降低性能表现。
2、我们使用的底层开发框架 mpvue 又针对了 setData 做了进一步封装,对于开发者而言 setData 基本上是黑盒过程,难以定位问题原因
为了解决这个问题,我们针对云医小程序上的 setData 过程从框架运行的角度进行了一次深入分析。
我们使用的 mpvue 是一个类 vue 的小程序跨端框架它借助了 vue 的响应式双向绑定、vnode 能力来优化小程序原生 setData。在进入页面的时候,会初始化一个 vue 实例,再初始化一个小程序的 Page 实例,在有数据变更的时候, vue 的数据响应层会收集数据更改并且提供给 render 函数,render 函数会生成 vnode。
正常情况下在 web 上,下一步就是将 vnode 映射为真实 dom,但是小程序并没有提供操作 dom 的 api,所以框架会给更新的值加一个脏检查标记,并且遍历一下 vnode,将有标记的数据组装成 json,然后传递给 Page 实例进行更新。
针对这个过程进行分析后,我们发现了两个问题:
1、由于进行数据脏检查标记的时候,数组类型对比的是 vm 中的值,Vue 不会保留变化之前数组的副本,因此数组类型无法做 diff ,所以数组类型的数据更新都是全量更新。
2、小程序里无法操作 dom。虚拟 dom 在这里属于一个多余设计。
我们还发现一个问题:mpvue 不支持小程序自定义组件。
这是因为在 mpvue 诞生之初,微信小程序尚不支持自定义组件,无法进行组件化开发, 为了解决这个问题,mpvue 将用户写的组件,编译为 wxml 中的 模版。看一个编译产出的例子:
通过 import 的方式,子组件 —— 父组件 —— page 会被最终编译为一个大的 template,在组件中定义的数据会被编译为 Page 中的数据,对组件进行数据更新也会基于路径映射调用 Page.setData。每个组件的局部更新都会成为页面级别的全局更新。
小程序的组件模型与 Web Components 标准中的 ShadowDOM 非常类似,每个组件都有独立的节点树,拥有各自独立的逻辑空间(包括独立的数据、setData 调用、createSelectorQuery 执行域等)。从页面级别的更新是要比组件级别的局部更新性能表现更差的。
到此为止,可以得出一个结论,mpvue 上的性能表现已经不满足我们的需要,由于项目是基于 vue 开发的,迁移到同类型的类 vue 语法小程序框架上的成本很低,因此,我们选择使用框架迁移来解决这个问题。
我们从增量更新,自定义组件,适配h5,社区活跃几个角度,对市面上几种其它的类 vue 语法小程序开发框架进行了调研和对比,最终选择了 uni-app 作为我们的开发框架。
图片
uni-app 带来的提升主要有:
- Vue 层取消 vnode 对比。
- 借鉴了 westore 的 JSON Diff 库,该库高效轻量,可以进行更彻底的 diff 计算,对于常熟组类型的数据也可以进行高效准确的 diff。
- 支持小程序自定义组件。
迁移完成后,我们以之前性能表现较差的会话页为例,进入会话页上拉历史记录,统计十三次拉取聊天记录的情况下的 setData 渲染耗时平均值,可以得到这样一个对比图表。
长列表场景下,mpvue 【setData 时间】与【拉取新数据次数】成正相关
uni-app 上 【seData 时间】与【拉取新数据次数】无明显关联,基本上稳定在 3ms 左右
在框架迁移后,随着 setData 的性能提升,长列表场景的卡顿问题已经得到了暂时的缓解,但是仍然没有从根本上解决问题,只要列表在加载新的内容,页面内容就会越来越多,dom 树结构就会越来越复杂,数据更新引起的 Recalculate Style 和 Layout 时间也会更长,最后 setData 性能还是退化。
针对这个问题。我们采用的优化思路是虚拟滚动:它的核心的思路是只渲染显示在屏幕的数据,仅更新局部可见区域,对于已经脱离了可视区域的列表项,改用空白节点占位。
这里主要遇到的困难有两个:
1、虚拟滚动的空白节点高度怎么算?
首先由开发者定义多少个列表项算一屏,把长列表的一维数组改成一个二维数组,二维数组的每一项对应了每一屏(不一定是真实的屏幕上的一屏,只要比一屏的长度长就行了),在渲染完成后,获取当前最新渲染这一屏的高度,将其赋值给用于记录每一屏高度的数组:pageHeightArr。然后在翻过当前一屏后,将其作为空白节点的高度计算标准。
2、虚拟滚动在快速滚动的时候怎么做?
在可视元素列表前后预先多渲染几个列表元素。这样我们在少量滚动时可以偏移这些已渲染的元素而不是重新渲染,当滚动量超过缓存元素时,再进行重新渲染。
优化收益:对比腾讯云医小程序->群发助手下的患者列表初始化和选中时接入长列表组件前后的对比,横坐标为列表数据条数,当列表数据较少的时候,虚拟滚动优化效果不大,当列表数据指数级别上升,从 100 到 10000 条的时候,setData 时长变成一条相当陡峭的曲线,而虚拟滚动几乎不变。另外运行内存的峰值也从 800M 下降到 500M
目前虚拟滚动组件已上传至npm,可以通过微信第三方组件形式引入
npm i mp-v-scroll
阶段性总结
就这样,做完这些性能优化之后,我们可以总结出一套性能优化的通用方**。分别是
- 轻量:需要尽量精简数据大小、资源体积、代码逻辑、接口请求等,这也是我们用到的最主要的性能优化方式。
- 提前:将页面渲染需要的环境、数据等提前准备好,例如数据预拉取、接口数据缓存等。
- 并行:减少没有必要的串行等待过程,例如在 page 创建的时候异步准备页面需要的数据。
- 实时:用户操作永远是最高优先级,需要给出实时的 ui 反馈,例如骨架屏、loading 信息等。
前三个是技术优化,最后一个是体验优化。
从这个通用思路拓展,我们一共做了这些事情
这里再简单介绍一些:
纯数据字段(轻量)
某些 data 中的字段(包括 setData 设置的字段)既不会展示在界面上,也不会传递给其他组件,仅仅在当前组件内部使用,最常见的就是那些用于页面逻辑判断的布尔值。
此时,可以指定这样的数据字段为纯数据字段,它们将仅仅被记录在 this.data 中,而不参与任何界面渲染过程,这样有助于提升减少页面 initialData 的体积,提升页面首次渲染速度。
纯数据字段 从小程序基础库版本 2.8.2 开始支持。
// 对所有页面开启,在app.json中配置,建议对所有页面开启
// 指定“纯数据字段”的方法是在 Component 构造器的 options 定义段中指定 pureDataPattern 为一个正则表达式,字段名符合这个正则表达式的字段将成为纯数据字段。
Component({
options: {
pureDataPattern: /^_/ // 指定所有 _ 开头的数据字段为纯数据字段
},
data: {
a: true, // 普通数据字段
_b: true, // 纯数据字段
},
methods: {
myMethod() {
this.data._b // 纯数据字段可以在 this.data 中获取
this.setData({
c: true, // 普通数据字段
_b: true, // 纯数据字段
})
}
}
})
初始渲染缓存(提前)
启用初始渲染缓存,可以使视图层不需要等待逻辑层初始化完毕,而直接提前将页面初始 data 的渲染结果展示给用户,这可以使得页面对用户可见的时间大大提前。它的工作原理如下:
在小程序页面第一次被打开后,将页面初始数据渲染结果记录下来,写入一个持久化的缓存区域(缓存可长时间保留,但可能因为小程序更新、基础库更新、储存空间回收等原因被清除);在这个页面被第二次打开时,检查缓存中是否还存有这个页面上一次初始数据的渲染结果,如果有,就直接将渲染结果展示出来;
// 对所有页面开启,在app.json中配置,建议对所有页面开启
{
"window": {
"initialRenderingCache": "static"
}
}
路由跳转预加载(并行)
背景:在小程序框架迁移结束后,我们观察到由于 uni-app 使用了小程序自定义组件的原因,页面初始化的时候开销增加了,页面首次平均渲染时间,从 50ms 提升到了 150ms。
页面首次渲染只是页面渲染出初始内容的时间,并不是我们最关注的性能数据,但是它的时长变长,也会给页面核心区域可交互时长带来退化。所以我们提供了路由跳转预加载的通用方法来优化这个问题。
适应场景:小程序页面跳转。
分析:从用户触发跳转行为到下一个页面加载完成的过程中,小程序需要完成一些环境初始化以及页面实例创建的工作,从线上监控来看,这段耗时大概 300 ~ 400 毫秒。一般页面会在 onLoad 钩子被触发时发起网络请求,但其实这并不是最快的方式。从上一个页面发起跳转(wx.navigateTo 调用前)的时候实际上就可以提前请求下一个页面的主接口并存储在全局 Promise 对象中,待下个页面加载完成后从 Promise 对象中读取数据即可。
效果:加上路由跳转预加载后,iphone11 页面核心区域可交互时长时间提升约 100ms ~ 300ms 小米 mix 提升 约 200ms。
分片渲染(实时)
云医上线后,曾经有一些医生反馈,首页用户操作偶尔没有反应,由于这个问题及其偶现,我们从监控数据入手,收集到首页 onshow 逻辑中,里面有几条 setData 时长特别长,接近 1000ms。
如果 setData 过于频繁或者数据量过大,会导致渲染线程繁忙。而我们能够操控的各种点击、滚动事件将拥堵在 webview js 线程上,得不到响应。从这个问题入手,我们分析了云医首页 onshow 中存在的问题。发现在 onshow 中存在着频繁的 状态更新:
任务:
- 抢单状态初始化
- 获取医生问诊排名数据
- 获取首页注册状态
- 获取首页填写状态
- 获取代办事项
- 获取消息提醒状态
- 获取锦旗数量
对于这种需要放在 onshow 中保证更新频率,但是对更新及时度又没有那么强的任务。我们可以采用分片渲染的方式,每执行完一个任务之后,把控制权从函数中移交出来,给渲染线程留下一部分空闲时间:
// gen 是一个 generator 函数
timeSlice (gen) {
if (typeof gen === 'function') gen = gen.call(this);
if (!gen || typeof gen.next !== 'function') return;
return function next () {
const res = gen.next();
if (res.done) return;
setTimeout(next, 16);
};
},
onshow() {
this.timeSlice(
async function * run () {
task1();
yield;
task2();
yield;
// ...
},
)();
}
进行分片渲染后,首页 setData 时长得到了有效的缓解。从 500ms 降至 100ms 附近
渐进图片渲染(实时)
对于大图资源和一些金刚位图片资源,我们可以先呈现高度压缩的模糊图片,同时利用一个隐藏的 <image> 节点来加载原图,待原图加载完成后再转移到真实节点上渲染。整个流程,从视觉上会感知到图片从模糊到高清的过程。有效减少首屏的白屏时间。
组件实现也很简单,通过 image 组件 的 load 事件来控制本地图片和 cdn 图片的展示即可:
// template
<image
v-show="isLoaded"
class="httpImage"
:src="httpPath"
@load="loaded()"
>
</image>
<image v-show="!isLoaded" class="localImage" :src="localPath"></image>
// script
methods: {
loaded () {
this.isLoaded = true
}
}
优化 - 总收益
到此为止,优化手段就介绍的差不多了。这是总的性能优化结果:
保障
在做性能优化的过程中,我们也注意到,随着业务的逐渐迭代,曾经优化过一轮的性能指标又面临了部分退化;如何在快速的版本迭代中做好高性能的保证 成为我们需要长期克服的难题。
针对这个问题,我们用到的主要方式是:
性能评分
为页面性能和小程序启动性能构建评分机制。
性能告警
- 耗时:利用大盘用户大数据,与历史版本进行比对分析。发现新增性能下降,采用周期性告警策略。
- 异常:前端异常 类型的数据,采用 TAM 趋势性告警 能力。
- 专项优化:建立页面性能负责人机制,对于新版本性能下降的页面进行页面专项优化。
结语
到此为止,整个云医业务小程序性能优化就讲完了,这次的性能优化,我们找寻了小程序运行中可能影响性能的点,再针对这些影响点我们提出可行的优化建议,落地方案,最终一个个小的优化汇合成一个大的结果,使小程序的性能得到大幅的改善。另外,我们还会常常收到很多真实的用户关于使用的反馈,这些是数据所无法反应出来的,所以我们设置了专门的用户体验问题群组,用于跟进用户体验问题并且从研发侧推动改进。
关于作者
田彧(yù),2015年毕业于华中科技大学,2020年加入腾讯至今,目前负责腾讯云医前端业务以及大后台运营系统开发,支持 CSIG 研发效能项目 APIops,在性能优化和研发效能等方向积累了一定的的实践经验。