导语 | 如果你的小程序也遇到了性能问题,我们的实践经验也许可以给到你启发,我们从小程序的启动、加载到交互都进行了探索。顺便说一句,这篇文章在腾讯内部曾被小程序技术总监打赏。
事情,要从一个周末惬意的下午开始说起……
那天,手机突然被唤醒,弹出多条微信消息。原来是这周末正在校园推广的活动群发来的,想起之前大家有条不紊的开发进度,和产品沟通的友好过程,应该是活动反响不错。
现实是残酷的:
“我们的小程序打开慢成狗!”
“这个 loading 加载的过程也太久了!”
“滚动加载有点卡,而且很容易报错……”
看到的是最直接的控诉。
看到用户的录屏,这几个问题确实是有出现,所以我们还是需要对小程序进行一次主流程的性能优化,三句控诉可以总结为3个点:
收到反馈后第一反应是,用户是不是网速太慢了,自己跑一遍,发现自己的手机跑起来都是没问题的,灰常流畅,下意识的可能想录个屏回复过去。
不过有用户录屏在那,当然不能这么草率,所以我们查了下管理后台小程序在不同网络下的大盘数据:
网络 | 启动耗时 |
---|---|
总体 | 3.6s |
WIFI | 3.5s |
4G | 3.9s |
2G/3G | 4.1s |
从统计看,总体3.7s的启动耗时,网络对于启动耗时是会有影响的,但影响没有很大,就算是2G-3G下跟大盘的数据对比也没有慢很多,可见事情并不简单。
于是我们从另外一个维度来看一下大盘数据:
机型 | 启动耗时 | JS注入 | 初次渲染 |
---|---|---|---|
总体 | 3.6s | 0.29s | 0.16s |
高端机 | 2.9s | 0.19s | 0.06s |
中端机 | 4.8s | 0.42s | 0.19s |
低端机 | 7.9s | 0.72s | 0.43s |
从这里就可以看出问题来了,手机的性能对于小程序的启动速度影响非常大,低端机相对高端机有2-3倍的差距,特别是渲染层甚至有5-6倍的差距,而且问题反馈的用户所使用的手机也确实是中低端机,但用户使用什么手机我们也没法控制,那这里有办法去优化吗?
针对这个问题,我们需要了解一下小程序的启动过程,根据官方文档的介绍,小程序的启动可以分为下面几个步骤:
上图描述了用户点击小程序开始到页面开始请求数据的一个完整的冷启动过程,而小程序初始化的过程(信息准备、环境准备)占用了比较长的时间,但这部分的工作是由微信客户端来完成,开发者无法干预,所以我们只能聚焦于后续的步骤(下载代码包、注入代码包、初次渲染)。
根据官方文档的介绍,这一部分的可优化手段有:
后面4条在技术上没有特别好的限制手段,需要我们在 Code Review 的时候对复杂度和开销大的接口调用进行把关,复杂度这里还可以借助如 CodeCC(腾讯内部代码检查工具) 这类工具去进行分析,减少自定义组件数量,这个是比较难以抉择的,需要在代码可读性、可复用性之间做个取舍,不是本次优化的重点。
所以我们就重点关注的代码包体积问题,通过我们的 CI 记录可以收集到我们的总包大小:
可以看到主包体积达到1949.71KB,接近了2M的极限了,在通过依赖分析后,发现除了一些是未使用到的模块和组件外,很大一部分内容是静态资源,同时我们也在官方文档中看到这样一句话:
小程序代码包在下载时会使用 ZSTD 算法进行压缩,这些资源文件会占用大量代码包体积,并且通常难以进一步被压缩,对于下载耗时的影响比代码文件大得多。
所以我们要减小代码包的体积,最直接的方法就是将非必要的资源去除掉:
同时我们还关注到,有一些分包特别小,但是由于是普通分包,在打开这些页面的时候还需要先下载主包,这里在包下载耗时上其实是有一些浪费的,比较典型的就是 WebView 页面,他们往往只需要对参数进行处理,对于主包的依赖不是很强,所以这里还有一个可以优化的点:
我们通过日志查到这个用户的首页数据请求返回会到3-4s,请求慢在正常情况下会有这么两种情况:
我们通过日志统计发现用户的访问时间端,请求量跟平时保持一致,看大盘请求耗时的统计,也没有产生大的波动:
所以基本可以排除是后台的问题,虽然大盘的数据在500ms左右,但是当用户网络不好的情况下,这一块要怎么保证呢?
答案当然是做提前拉取,当用户冷启动的时候,我们可以使用小程序官方提供的数据预拉取能力提前拉取,从小程序的启动耗时看,完全可以 cover 掉我们的接口请求耗时,可以让小程序启动成功后就直接渲染页面。
在热启动的情况下,请求慢主要体现在用户交互时发生的请求和页面切换时发生的请求,交互的情况我们下一节在分析,这里主要看页面切换,从我们的统计数据来看,页面切换的耗时大概在400ms左右,而其中能够利用的时间大概是50ms-100ms:
route时间
利用页面切换的这个时间提前对页面的数据进行加载,可以减少用户感观上的数据请求时间,同时在第一次请求之后可以根据一定的策略对页面的数据进行缓存,从而可以达到二次进入页面秒开的效果。
总结来看,请求慢的优化手段有下面几个,而且理论上效果都会很显著:
先说一下这里的交互慢具体指什么,我们收到用户反馈的现象是:用户首屏顺利加载出来之后,后续滚动加载和一些按钮点击的响应非常慢并且很容易报错。收到这个反馈后定位了很久,讲道理如果是因为用户网络问题导致的请求很慢,应该所有的请求都会很慢,但是用户表现出来的现象是后续的加载以及交互很慢,反而首屏还算正常。
通过日志查询,我们发现这个用户的请求报错都是请求超时,为什么超时会集中在交互加载这里呢?定位了一段时间后我们发现一个用户的报错都集中在首屏加载之后就立马下滑或者点击,如果过了一段时间再点击又不会报错。
发现这个现象后,我们想到了官方文档关于网络使用说明的一个限制:
wx.request、wx.uploadFile、wx.downloadFile的最大并发限制是 10 个
再结合我们对于 wx.request 的封装,请求超时的计时器是从调用 wx.request 的时候就开始了,如果请求并发超过了限制,那么就很容易出现请求超时,而当我们从第一个业务接口请求到数据后就会进行一系列的数据上报,包括 pv、组件曝光、关键链路打点等等,所以我们利用 Whistle 的 resDelay 方法,将我们的上报请求都延迟5000ms返回,果然就复现了用户反馈的那种情况。
找到问题之后也就明确了需要优化的方向:
交互慢还有别的原因吗?在继续挖掘性能瓶颈的过程中,发现腾讯课堂小程序的课程详情页内容非常多,有5-6屏的高度,而用户只会关心首屏是不是更快的呈现出来,但是我们原本的处理方式是比较粗暴的,拿到详情页的数据之后对数据进行处理,格式化成整个页面所需要的数据之后一次性调用 this.setData
来更新页面,所以如果要提升首屏速度,这里需要做的就是:
再归总一下需要优化的点和方向:
2 . 请求慢主要从预加载和缓存下手:
3 . 交互慢需要从发起请求和页面渲染下手:
由于用户反馈主要是因为校园推广活动来的,而活动页面我们是通过 WebView 内嵌 H5 来承载的,而 WebView 页面的启动过程和小程序原生页面还不太一样:
实际上 WebView 页面只需要完善登录态传递的功能,对于主包的依赖不是很大,而且这部分页面更大的性能问题需要在 h5 那边来优化,所以我们第一时间对其进行了独立分包处理。
最终的优化效果还不错,因为启动过程中不需要下载主包,启动性能提升了30%。
我们小程序构成主要是由原生页面 + kbone 页面组成的,kbone 是采用的官方的方案,通过 webpack 构建,有很多单独打包静态资源的方案。而我们的原生页面是使用 gulp 进行构建的,原来主要的功能是将源码中的 ts 转成 js,同时对 css 文件通过 postcss 转成 wxss,由于 wxss 不支持引用相对路径,所以在 wxss 中引用的图片和字体都是转成 base64 的,然后对其余的文件如 json、wxml 文件则直接复制到产物中去。
这样的处理方式比较粗暴,通过 postcss 将 background-image 所引用的本地图片都转成 base64,还会导致很多图片在项目中占用了2倍的体积。
CI流程-优化前
所以我们首先需要将源码下的静态资源匹配到并单独构建出来,并且为了规避同名文件的问题,需要对资源打个 hashtag,我们这里需要用到一个 gulp 插件gulp-rev
,这个插件可以对基于资源的内容进行 hash。
CI流程-优化
后将图片上到 CDN 后,把 css、js、json、wxml 中的引用路径替换成 CDN 地址,具体的替换逻辑如下。
CDN流程
随着业务的迭代,不可避免的会有一些组件被废弃了但是难以察觉,通过我们团队开发的小程序脚手架 imweb-miniprogram-cli 对页面所使用到的组件进行分析,可以把项目中未使用到的组件给过滤出来,不会打包到最终的产物中,大致的思路如下:
组件依赖
从app.json
开始,拿到小程序所配置的所有页面和分包,通过检查 App、页面、分包中所使用的自定义组件来进行收集,并且递归检查自定义组件所使用的组件,如果检测到有未使用的组件,也会给到提示,非常友好:
组件过滤
可以看到在我们的项目中就发现了好几个未使用到的组件。
数据预拉取需要在小程序的管理后台开启,数据来源可以选择开发者服务器或者云开发,选择开发者服务器的话会有一些限制,如果是直接填写 CGI 地址,就只能拉取一种数据,不够灵活,而如果再搭建一个服务去做预拉取涉及到的工作量又会很大,所以我们选择的是云开发的方式,大致流程如下图:
数据预拉取-大概
当小程序启动的时候,微信客户端会根据配置去拉取指定的云函数,在云函数中通过 cl5 调用业务后台的服务拉取到需要的数据,拉取到后客户端会将数据缓存在本地,当小程序启动成功后,在业务代码中调用wx.getBackgroundFetchData
就可以拿到预拉取的数据,如果缓存数据拉到的是所需要的数据则可以直接渲染,如果不是则降级到业务中再拉一次接口。
在云函数中可以拿到本次小程序启动的path
和query
参数,所以我们可以根据这两个参数来判断本次预拉取需要调用业务后台的哪个服务,从而达到从不同的页面启动小程序都可以通过一个云函数预拉取到所需要的数据。
const preFetchMap = {
'pages/index/index': fetchIndex,
'pages/course/course': fetchCourse,
}
// 云函数入口函数
exports.main = async (event) => {
const { path, query = '' } = event;
const fetchFn = preFetchMap[path];
if (fetchFn) {
const res = await fetchFn(query);
return res;
}
return {
error: {
event,
retcode: -1002,
msg: `${path}页面未设置预拉取逻辑`
}
};
};
不过要注意的是,因为小程序自身做了很多初始化的优化,有可能在小程序启动后,预拉取的数据还没有返回,所以我们做了进一步的优化,在业务拉取的过程中通过 wx.onBackgroundFetchData
监听预拉取的返回,收到返回就直接渲染 ,尽可能的使用预拉取的数据来渲染首屏。
数据预拉取
前面已经提到过,提前拉取就是要利用小程序切换页面的空隙开始拉取数据,从而在感官上较少数据请求的时间,整体的逻辑是通过封装的跳转逻辑,对应的页面添加不同的数据拉取逻辑,并将拉取的 promise 挂载在 App 上,当页面切换完成后优先使用 App 上的 promise 来获取数据。
数据缓存则是在数据拉取成功后,将比较固定的数据通过 wx.setStorage
缓存在本地,当第二次切换到这个页面时,先使用本地缓存的数据进行渲染,后面再通过拉取的数据来进行更新。
提前拉取
保障业务请求的核心思路是让业务请求优先,我们封装了一个 排队请求模块 ,通过对 wx.request
API的拦截,将请求根据配置有个优先级排序,低优先级的会在请求并发数达到一定的阈值之后被推到等待队列 WaitingQueue
中,留出足够的通道给到高优先级的业务请求。
请求排队
这里的方案相信大家也能很好理解,主要是优先处理首屏需要的数据并通过 setData
更新视图,然后在处理其余的数据。但根据官方文档的说明:
setData
函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的this.data
的值(同步)。
而小程序代码执行顺序也遵循JS的事件循环机制,仅仅是在处理后的数据调用 setData
,然后继续或者通过 Promise
处理下一步的话,并不能达到分步渲染的目的,而直接通过回调的方式在 setTimeout
中使用嵌套渲染,代码的可读性会变差,同时也不是很优雅。我们的解决方式是利用 setTimeout
封装了一个符合 Promise 标准的方法,从而可以像使用 Promise 那样继续分步渲染:
经过这一系列的优化,效果还是比较明显的:
在包大小方面:
从启动耗时的数据看,下载耗时和JS注入耗时都有明显的下降:
启动耗时
再看打开耗时分布,可以看到3s内打开的用户比例有明显增加,从56.26%增加到64.25%;
打开分布
数据预拉取,提前拉取,数据缓存在冷启动和页面切换时都起到了很不错的效果:
首页请求速度从平均400ms下降到50ms,优化了87.5%;
课详页的请求速度从平均800ms下降到90ms,优化了88.75%;
而数据缓存让二次访问时页面可以秒开:
二次加载
使用排队请求之后,对网络请求顺序的干预效果还比较明显,灰度用户业务请求耗时平均有50-100ms,约15%的优化;
同时我们通过分析耗时分别在80分位、50分位、20分位的效果发现,请求耗时越长,优化效果越明显,也就是说在弱网情况下能够更好的发挥作用。
请求排队结果
使用分步渲染后,我们的页面可以在处理完首屏的基础数据之后就立即开始渲染了,由于我们的目录结构比较复杂,处理起来耗时比较长,所以第二部才处理目录,实际的渲染效果如下图:
分步渲染
首屏可以比原来提前100ms-150ms渲染出来。
我们本次的性能优化对小程序启动、请求、交互、渲染多个方面都进行了性能的挖掘,是在对基础库版本要求不高的情况下能做到的极致了。
以我们的核心页面首页和课程详情页来说:
还有更多的优化手段吗?官方还提供了一些比较高级的功能,对基础库版本要求比较高的,例如:
利用这些能力可以在更多细节上进行优化,我们也将进一步探索和跟进,如果你有更好的方案欢迎讨论。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ejzY97L80azy06fP3UzmDw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。