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

8,868 阅读11分钟

小程序渲染原理

render

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

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

影响性能的关键因素

  1. 频繁的去setData

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

    • Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;
    • 渲染有出现延时,由于 WebViewJS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;
  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中的bindcatch),从而减少通信的数据量和次数

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页面的资源。

img

8. 谨慎使用onPageScroll

pageScoll事件,也是一次通讯,是webview层向js逻辑层的通讯。这次通讯开销较大,如果考虑到这个事件被频繁的调用,回调函数如果有复杂的setData的话性能就会变得很差。

img

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()的参数中,有生命周期的方法:onLoadonShowonReadyonHideonUnload

页面触发的第一个生命周期回调是onLoad,在页面加载的时候触发,其参数是页面的query参数,一个页面只有一次;

接着是onShow,监听页面的显示,与onLoad不同,如果页面被隐藏后再次显示(例如:进入下一页后返回),也会触发该生命周期;

触发onShow之后,逻辑层会向渲染层发送初始化数据,渲染层完成第一次渲染之后,会通知逻辑层触发onReady生命周期,一个页面只有一次;

onHide是页面隐藏但未卸载的时候触发的,如 wx.navigateTo 或底部tab切换到其他页面,小程序切入后台等。

onUnload是页面卸载时触发,如wx.redirectTowx.navigateBack到其他页面时。

img

整体周期

打开页面的情况

首先,前一个页面隐藏,在加载下一个页面之前,需要先初始化新页面的组件。页面首次渲染之后,会触发组件的ready,最后触发的是页面的onReady,如下图:

img

从PageA打开pageB时的生命周期顺序

离开页面的情况

离开当前页面时,首先触发当前页面的卸载onUnload,接着是组件离开节点树的detached。最后显示之前的页面,触发onShow。如下图:

img

从PageB返回到PageA的生命周期顺序

切换到后台

切换到后台时,小程序和页面并没有卸载,只会触发隐藏。先触发页面的onHide,接着是App的onHide。如下图:

img

切换到后台时的生命周期顺序

切换到前台

切换到后台时,小程序会先触发onShow,之后才是页面的onShow。如下图:

img

切换到前台时的生命周期顺序

关键性能指标

了解了小程序各个阶段的生命周期,我们可以制定出关键节点的性能指标,整理如下表:

img

记录数据

如果我们记录下每一个页面的优化前后的可交互时间数据,并且对比,可以很好的分析每一个页面的性能提升有多少,从而判断自己有没有在做无用功。

从上面的关键性能指标中,抽取可交互时间作为本次的重要评估指标之一,即从小程序页面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

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

优化心得

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

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