用户
 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,登录网站

小程序社区 首页 资讯/观点 查看内容

微信小程序渲染性能优化总结

Rolan 2019-8-12 00:16

双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层, ...

双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把 WXML 转化成对应的 JS 对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的 setData 方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。

由此可得:页面初始化的时间大致由 页面初始数据通信时间 和 初始渲染时间 两部分构成。其中,数据通信的时间指数据从逻辑层开始组织数据到视图层完全接收完毕的时间,传输时间与数据量大体上呈现正相关关系,传输过大的数据将使这一时间显著增加。 因而减少传输数据量是降低数据传输时间的有效方式 。

影响性能的关键因素

  1. 频繁的去 setData

    ​ 如果非常频繁(毫秒级)的去 setData ,其导致了两个后果:

    • Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;
    • 渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;
  2. 每次 setData 都传递大量新数据

    ​ 由 setData 的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大 时会增加脚本的编译执行时间,占用 WebView JS 线程。

  3. 后台态页面进行 setData

    ​ 当页面进入后台态(用户不可见),不应该继续去进行 setData ,后台态页面的渲染用户是无法感受的, 另外后台态页面去 setData 也会抢占前台页面的执行。

  4. 在 data 中放置大量与界面渲染无关的数据

优化方案

1. 减少 setdata 的数据量

​ 如果一个数据不会影响渲染层,则不用放在 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生命周期

在确定性能指标前,有必要对小程序页面的生命周期做一个梳理。

在每个页面注册函数 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)
小程序开发工具400130180
小米8 SE(扫二维码真机调试模式)30001101000

其中,扫二维码真机调试模式由于其本身问题,时间变长为正常现象,在自动真机调试模式中,各项指标恢复正常,但由于没有确切数据,故不列入表格中。

优化心得

自此,线路展示页面的性能优化完成,在实际优化过程中,发现性能影响最大的就是下面的问题

  • 下拉加载更多,特别特别卡,开始以为是因为随着下拉page维护的数据越来越多导致,在减少了page实例中维护的数据后,发现性能改善不大。 后来发现,是因为需要监听scroll事件,导致scroll事件被频繁的触发,回调函数中有耗时操作,导致 onreachBottom 事件被阻塞了,也就是说,要等大概1~2秒才会去发起下一页的请求。 取消掉 scroll 事件的监听,性能就大大提升了。归根结底还是对小程序的 api 不熟悉,为了获得滚动条高度而频繁监听 scroll事件,可谓是本末倒置。
鲜花
鲜花
鸡蛋
鸡蛋
分享至 : QQ空间
收藏
原作者: Gavin_Li 来自: 掘金