双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML
转化成对应的JS
对象,在逻辑层发生数据变动的时候,咱们须要经过宿主环境提供的setData
方法把数据从逻辑层传递到渲染层,再通过对比先后差别,把差别应用在原来的Dom树上,渲染出正确的UI界面。javascript
由此可得:页面初始化的时间大体由页面初始数据通讯时间和初始渲染时间两部分构成。其中,数据通讯的时间指数据从逻辑层开始组织数据到视图层彻底接收完毕的时间,传输时间与数据量大致上呈现正相关关系,传输过大的数据将使这一时间显著增长。于是减小传输数据量是下降数据传输时间的有效方式。java
频繁的去setData
web
若是很是频繁(毫秒级)的去setData
,其致使了两个后果:小程序
JS
线程一直在编译执行渲染,未能及时将用户操做事件传递到逻辑层,逻辑层亦没法及时将操做处理结果及时传递到视图层;WebView
的 JS
线程一直处于忙碌状态,逻辑层到页面层的通讯耗时上升,视图层收到的数据消息时距离发出时间已通过去了几百毫秒,渲染的结果并不实时;每次setData
都传递大量新数据api
由setData
的底层实现可知,咱们的数据传输实际是一次 evaluateJavascript
脚本过程,当数据量过大 时会增长脚本的编译执行时间,占用 WebView JS
线程。数组
后台态页面进行 setData
缓存
当页面进入后台态(用户不可见),不该该继续去进行setData
,后台态页面的渲染用户是没法感觉的, 另外后台态页面去setData
也会抢占前台页面的执行。性能优化
在data
中放置大量与界面渲染无关的数据网络
1. 减小setdata
的数据量app
若是一个数据不会影响渲染层,则不用放在setData
里面
2. 合并setdata
的请求,减小通信的次数
避免过于频繁调用setData
,应考虑将屡次setData
合并成一次setData
调用
// 不要频繁调用setData
this.setData({ a: 1 })
this.setData({ b: 2 })
// 绝大多数时候可优化为
this.setData({ a: 1, b: 2 })
复制代码
3. 去除没必要要的事件绑定(WXML
中的bind
和catch
),从而减小通讯的数据量和次数
4. 避免在节点的data
的前缀属性中防止过大的数据
5. 列表的局部更新
在一个列表中,有n
条数据,采用上拉加载更多的方式,假如这个时候想对其中某一个数据进行点赞操做,还能及时看到点赞的效果。
- 能够采用
setData
全局刷新,点赞完成以后,从新获取数据,再次进行全局从新渲染,这样作的有点是:方便,快捷!缺点是:用户体验极其很差,当用户刷量100多条数据后,从新渲染会出现空白期。
- 也能够采用局部刷新,将点赞的
id
传过去,知道点的是哪一条数据,从新获取数据,查找相对应id
的那条数据的下标(index
是不会改变的),用setData
进行局部刷新,如此,即可以显著提高渲染速度。
this.setData({
list[index]=newList[index]
})
复制代码
6. 与界面渲染无关的数据最好不要放置在data
中,能够考虑设置在page
对象的其余字段下
Page({
onShow: function() {
// 不要设置不在界面渲染时使用的数据,并将界面无关的数据放在data外
this.setData({
myData: {
a: '这个字符串在WXML中用到了',
b: '这个字符串未在WXML中用到,并且它很长…………………………'
}
})
// 能够优化为
this.setData({
'myData.a': '这个字符串在WXML中用到了'
})
this._myData = {
b: '这个字符串未在WXML中用到,并且它很长…………………………'
}
}
})
复制代码
7. 防止后台页面的js
抢占资源
小程序中可能有n个页面,全部的这些页面,虽然都拥有本身的webview
(渲染层),可是却共享同一个js
运行环境。也就是说,当你跳到了另一个页面(假设是B页面),本页面(假设是A页面)的定时器等js
操做仍在进行,而且不会被销毁,而且会抢占B页面的资源。
8. 谨慎使用onPageScroll
pageScoll
事件,也是一次通信,是webview
层向js
逻辑层的通信。此次通信开销较大,若是考虑到这个事件被频繁的调用,回调函数若是有复杂的setData
的话性能就会变得不好。
9. 尽量使用小程序组件
自定义组件的更新只在组件内部进行,不受页面其余内容的影响,各个组件也将具备各自独立的逻辑空间。每一个组件都分别拥有本身独立的数据,setData
调用。
检查点:是否频繁去setData
检查结果:暂无
检查点:每次setData
都传递大量新数据
检查结果:暂无
检查点:后台态页面进行setData
检查结果:存在
产生缘由:因为改操做处于搜索页面中,在用户点击后便马上返回到上一级页面进行数据展现,故在后台态页面进行setData
,提升跳转页面的速度。
问题代码:
// pages/line/searchResult/searchResult.js
showSearchDetail(e){
...省略代码
let prevpage = this.getPrevPage()
prevpage.setData({
isInSearch: true,
showResult,
keyWord:res.routeName
})
}
复制代码
检查点:在data
中放置大量与界面渲染无关的数据
检查结果:存在
产生缘由:因为当前请求的用以查询线路信息的接口GET/api/Route/List/{cityid}/{pagesize}/{pageno}
不支持分页请求,会一次性返回全部数据,因此在以前的方案中,为了减小请求产生的网络流量,会一次性把全部数据暂存到页面的一个数组中,(该数组存储大概600多个对象),而后再根据需求展现部分数据。
问题代码:
getLinesInformation(cityID) {
return new Promise((resolve, reject) => {
smartProxy.getRequest(`/Route/List/${cityID}/10/0`)
.then(res => {
this.data.lineArray = res
if (this.data.lineArray)
resolve()
else reject()
})
})
},
复制代码
解决方案:
方法一:用流量换性能 不暂存所有线路的信息,改而每次出现分页请求时重复请求该api
,更新页面展现所需的数组的数组。
缺点:重复请求api
获取相同的数据,浪费流量
优化效果:
在首次跳转搜索页时,耗时500ms
,再之后每次跳转搜索页耗时90ms
,下拉分页加载平均400ms
一页
方法二:改进存储方案 当请求到线路api
返回的数据后不放置在data
字段,改成设置在page
对象的其余字段进行存储。
优势:减小页面负担,优化性能
代码实现:
getLinesInformation(cityID) {
return new Promise((resolve, reject) => {
smartProxy.getRequest(`/Route/List/${cityID}/10/0`)
.then(res => {
//用lineArray字段存储请求得来的数据
this.lineArray = res
if (this.lineArray)
resolve()
else reject()
})
})
},
复制代码
检查点:不当使用onPageScroll
检查结果:存在
产生缘由:本意是为了实现用户在查看线路搜索结果后返回线路展现主页时可以返回到上次浏览位置,因此使用onPageScroll
事件获取滚动高度ScrollTop
,而后存储。但若是经过onPageScoll
事件获取的话至关于每次混动都会触发存储,严重影响页面效果。
问题代码:
onPageScroll: function (e) {
// 页面滚动时执行
// console.log(e);
if (e.scrollTop != 0 && !this.data.isInSearch && !this.data.keyWord) {
//设置缓存
wx.setStorage({
key: 'lineSearchScrollTop',
// 缓存滑动的距离,和当前页面的id
data: e.scrollTop
})
}
},
复制代码
解决方案:
wx.createSelectorQuery().selectViewport().scrollOffset
获取滚动条高度,而且只在用户点击搜索框跳转到搜索页面的时候才调用,减小onPageScroll
事件对页面性能的影响//获取滚动条高度
getScrollTop () {
let that = this
return new Promise((resolve, rej) => {
wx.createSelectorQuery().selectViewport().scrollOffset(function (res) {
that.setData({
scrollTop: res.scrollTop
})
resolve()
}).exec()
})
}
复制代码
在肯定性能指标前,有必要对小程序页面的生命周期作一个梳理。
在每一个页面注册函数Page()
的参数中,有生命周期的方法:onLoad
、onShow
、onReady
、onHide
、onUnload
。
页面触发的第一个生命周期回调是onLoad
,在页面加载的时候触发,其参数是页面的query参数,一个页面只有一次;
接着是onShow
,监听页面的显示,与onLoad
不一样,若是页面被隐藏后再次显示(例如:进入下一页后返回),也会触发该生命周期;
触发onShow
以后,逻辑层会向渲染层发送初始化数据,渲染层完成第一次渲染以后,会通知逻辑层触发onReady
生命周期,一个页面只有一次;
onHide
是页面隐藏但未卸载的时候触发的,如 wx.navigateTo
或底部tab切换到其余页面,小程序切入后台等。
onUnload
是页面卸载时触发,如wx.redirectTo
或wx.navigateBack
到其余页面时。
首先,前一个页面隐藏,在加载下一个页面以前,须要先初始化新页面的组件。页面首次渲染以后,会触发组件的ready
,最后触发的是页面的onReady
,以下图:
从PageA打开pageB时的生命周期顺序
离开当前页面时,首先触发当前页面的卸载onUnload
,接着是组件离开节点树的detached
。最后显示以前的页面,触发onShow
。以下图:
从PageB返回到PageA的生命周期顺序
切换到后台时,小程序和页面并无卸载,只会触发隐藏。先触发页面的onHide
,接着是App的onHide
。以下图:
切换到后台时的生命周期顺序
切换到后台时,小程序会先触发onShow
,以后才是页面的onShow
。以下图:
切换到前台时的生命周期顺序
了解了小程序各个阶段的生命周期,咱们能够制定出关键节点的性能指标,整理以下表:
若是咱们记录下每个页面的优化先后的可交互时间数据,而且对比,能够很好的分析每个页面的性能提高有多少,从而判断本身有没有在作无用功。
从上面的关键性能指标中,抽取可交互时间做为本次的重要评估指标之一,即从小程序页面
onload
事件算起,页面发起异步请求,请求回来后,把数据经过setData
渲染到页面后,上述一整个流程所花费的时间。
可是,一个小程序项目每每会有不少个页面,手动记录每个小程序的首屏时间,很麻烦。
所以,咱们能够改写this.setData
方法,加入上报时间点逻辑。
this._startTime = new Date().getTime();
let fn = this.setData;
this.setData = (obj = {}, handle = '') => {
let now = new Date().getTime();
// 上报渲染所须要的时间
log(now - this._startTime)
fn.apply(this, [obj, handle]);
};
复制代码
另外,还有一些记录性能指标须要记录,在本次的班车线路展现页面中,当用户下拉触底时的分页加载时间和从点击搜索框到搜索页面加载出来的时间也是咱们本次重要的性能评估标准。对于这种自定义场景,咱们能够利用console.time()
和console.timeEnd()
这一对函数来记录。
测试平台:小米8 SE、小程序开发工具
测试流程:首页 -> 线路 -> 下拉触底 -> 点击搜索框
测试指标:可交互时间,分页加载时间、页面跳转时间
优化后指标:
平台 | 可交互时间(ms) | 分页加载时间(ms) | 页面跳转时间(ms) |
---|---|---|---|
小程序开发工具 | 400 | 130 | 180 |
小米8 SE(扫二维码真机调试模式) | 3000 | 110 | 1000 |
其中,扫二维码真机调试模式因为其自己问题,时间变长为正常现象,在自动真机调试模式中,各项指标恢复正常,但因为没有确切数据,故不列入表格中。
自此,线路展现页面的性能优化完成,在实际优化过程当中,发现性能影响最大的就是下面的问题
onreachBottom
事件被阻塞了,也就是说,要等大概1~2秒才会去发起下一页的请求。 取消掉scroll
事件的监听,性能就大大提高了。归根结底仍是对小程序的api
不熟悉,为了得到滚动条高度而频繁监听scroll
事件,可谓是本末倒置。