微信小程序的性能优化

1,387 阅读11分钟

小程序如何进行性能优化?

官方建议从这三方面进行优化:

代码包大小优化

启动性能优化 渲染性能优化

image.png

启动性能优化

小程序在整个启动流程中,一般需要完成几项工作:

1.准备运行环境(微信自己处理的)

2.下载,注入并执行对应小程序代码包

3.渲染小程序首页

image.png

控制包大小

提升体验最直接的方法是控制小程序包的大小,这是最显而易见的

勾选开发者工具中“上传代码时,压缩代码”选项; 及时清理无用的代码和资源文件(包括无用的日志代码) 减少资源包中的图片等资源的数量和大小(理论上除了小icon,其他图片资源从网络下载),图片资源压缩率有限 分析我们使用的 node_modules的某些包的大小,看看有没有更优化的方案(moment的语言)

分包和分包优化

小程序在首次打开时,会去下载并执行代码包,随着代码包大小的上升,耗时也会相应增加。可以采取以下方案:

image.png

对开发者而言,能使小程序有更小的代码体积,承载更多的功能与服务;而对用户而言,可以更快地打开小程序,同时在不影响启动速度前提下使用更多功能。 建议开发者按照功能的划分,拆分成几个分包,当需要用到某个功能时,才加载这个功能对应的分包。

分包限制须知

因为分包限制,tabBar页面必须在app(主包)内,然而我们新增的模块,首页就在tabBar, 所以除了新增模块的首页放到主包内,它页面都放到 packageWork 新包内。


├── app.js

├── app.json

├── app.wxss

├── packageWork

│  └── pages

│    ├── field_clock

│    └── field_object

├── pages

│  ├── work

│    ├── field_clock

│  └── logs

└── utils

开发者通过在 app.json subpackages 字段声明项目分包结构:


{

 "pages": [

  "pages/index/index",

  "pages/volunteer-index/index",

  "pages/my/index",

  "pages/work/index"

  …

 ],

 "tabBar": {

  "list": [

//    …

   {

​    "pagePath": "pages/my/index",

​    "text": "我的",

​    "iconPath": "/img/bar/my.png",

​    "selectedIconPath": "/img/bar/my_cur.png"

   }

  ]

 },

 "subpackages": [

  {

   "root": "package-work",

   "pages": [

​    "pages/field_object/index",

​    "pages/add_field_object/index",

​    "pages/field_clock/index"

   ]

  }

 ]

}

subpackages 中,每个分包的配置有以下几项:

采用分包预下载

image.png

在tabBar里配置的,这里配置当打开主包 work首页 的时候,进行工作模块分包预下载。

"preloadRule": {
    "pages/work/index": {
      "network": "all",
      "packages": [
        "package-work"
      ]
    }
}

分包预下载是为了解决首次进入分包页面时的延迟问题而设计的。如果能够在用户进入分包页面之前就预先将分包下载完毕,那么进入分包页面的延迟就能够尽可能降低。

独立分包

小程序中的某些场景(如广告页、活动页、支付页等),通常功能不是很复杂且相对独立,对启动性能有很高的要求。使用独立分包,可以独立于主包和其他分包运行。从独立分包中页面进入小程序时,不需要下载主包。

image.png

建议开发者将部分对启动性能要求很高的页面放到特殊的独立分包中。

首屏渲染优化

大部分小程序在渲染首页时,需要依赖服务端的接口数据,接口请求放到页面的生命周期 onLoad 中,而不是 onReady里。

监听到页面加载,就校验登录情况,请求页面数据

onLoad: function (options) {
    app.checkAuth((error, token) => {
      if (error) {
        return
      }
      // 请求该页面的数据
    })
  },

缓存请求数据

小程序提供了wx.setStorageSync等异步读写本地缓存的能力,数据存储在本地,返回的会比网络请求快。

登录成功后将用户的token,以及用户信息都可以缓存到本地,记得退出登录的时候清楚缓存

export function set(session) {
  const localSession = Object.assign({}, session, {
    expires_timestamp: getExpireTimestamp(session.expires_in)
  });
  wx.setStorageSync(SESSION_KEY, localSession);

  _token = session.access_token;
}

export function clear() {
  wx.removeStorageSync(SESSION_KEY);
  clearTimeout(refresh_timer);

  _token = null;
}

精简首屏数据

推荐开发者延迟请求非关键渲染数据,缩短网络请求时延,与视图层渲染无关的数据尽量不要放在 data 中,以免传输垃圾数据,加快首屏渲染完成时间。

避免阻塞渲染

在小程序启动流程中,会顺序执行app.onLaunch, app.onShow, page.onLoad, page.onShow, page.onReady,所以,尽量避免在这些生命周期中使用Sync结尾的同步API,如 wx.setStorageSync,wx.getSystemInfoSync 等。

渲染性能优化

小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。

image.png

常见的 setData 操作错误

1. 频繁的去 setData

导致了两个后果:

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

2. 每次 setData 都传递大量新数据

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

3. 后台态页面进行 setData

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

A页面上有个定时器,此时打开了B页面,A页面的定时器还在运行,继续抢占B页面的资源,B页面卡顿了,但是并不是B页面的造成的性能问题,这种问题就不太好排查。希望大家都能做个有始有终的人,定时器不用了要清除。下面demo,定时器在 onHide 时要清除掉。切记切记

/**
   * 生命周期函数--监听页面显示
   */
onShow: function () {
    clearTimeout(getTodaytime)
    this.updateNowTime()
},

/**
   * 生命周期函数--监听页面隐藏
 */
onHide: function () {
    // 取消定时器 防止小程序内存不足,崩溃
    clearTimeout(getTodaytime)
},
updateNowTime() {
    getTodaytime = setInterval(() => {
      const myDate = new Date(); 
      const hours = myDate.getHours())
      const minutes = myDate.getMinutes())
      const seconds = myDate.getSeconds())

      const newTime = hours + ':' + minutes + ':' + seconds;
      this.setData({
        newTime: newTime
      })
    }, 1000)
  },

用户事件使用不当

过多的使用bindTap、bindCatch 不当的使用onPageScroll

image.png

使用自定义组件

在需要频繁更新的场景下,自定义组件的更新只在组件内部进行,不受页面其他部分内容复杂性的影响。

我们项目小程序打卡功能,需要展示当前到时间:时分秒,此处用定时器实现的,需要频繁的更新setData,此处就适合将该定时器提取为组件,让其在组件内部数据更新,不影响页面的其它部分。

小技巧

存在setData的数据过大

我们的功能里面有个滚动到底部加载的功能,优化前我们的做法是这样的

<!--只阐述逻辑,非真实代码-->

// 1: 初始一个list,存储列表数据
data = startList
// 2: 监听滚动事件,滚动到底部获取新数据,并追加到list尾部,最后重新setData
onReachBottom:()=>{
    const {list} = this.data
    fetchNewData().then((res)=>{
        list.push(res.list);
        this.setData({list})
    }
}

我估计大部分人面对长列表滚动的时候,一开始的处理方式都是这样的,如果数据不多,只有几页可能不会太暴露问题,如果页数过多,几十页甚至上百页的情况,list的数据会越来越大,每次setData的数据就会越来越多,因而每次页面重新渲染的节点就会越来越多,从而导致滚动到后面,加载越来越慢。另外,由于小程序的视图渲染层和数据逻辑处理层是分开的,不是在同一个线程上面的,从用户触发页面交互,到处理数据逻辑,最后层现页面,数据到视图是需要传输的,因而小程序本身对数据大小也有限制,不能超过1M。

setData数据路径

怎么解决呢?小程序setData里面的key支持数据路径的写法,比如

注意这里必须要是字符串

let o = obj;
this.setData({
    'o.属性':value
})

或者
let a = array;
this.setData({
    'array[0].text':value // 注意这里必须要是字符串
})

所以我们可以通过数据路径的写法,来将数据分批的传输到视图层中,减少一次性setData的数据大小。具体写法如下

// 1.通过一个二维数组来存储数据
let feedList = [[array]];

// 2.维护一个页面变量值,加载完一次数据page++
let page = 1

// 3.页面每次滚动到底部,通过数据路径更新数据
onReachBottom:()=>{
    fetchNewData().then((newVal)=>{
        this.setData({
            ['feedList[' + (page - 1) + ']']: newVal, // 注意这里是 字符串
        })
    }
}
// 4.最终我们的数据是[[array1],[array2]]这样的格式,然后通过wx:for遍历渲染数据

存在短时间内发起太多图片请求(图片懒加载)

一次性发送了过多的图片请求,导致了同一时间发起了过多的http请求,http连接是非常耗时的,尤其是一次性发起这么多,并且一次性发起的http链接也是有限制的,比如chrome浏览器就限制一次性最多6个。

所以在渲染页面时,不在视图范围内的图片我们不加载,只有元素出现在视图范围内了,再渲染。

常规的做法是,通过getBoundingClientRect()获取元素的位置,然后与页面滚动位置比较,如果出现在视图内,就将img显示。这种方式有2个问题

  • getBoundingClientRect()方法调用本身容易引起页面重排
  • 监听滚动事件本身就频繁触发,虽然可以通过节流的方式来减少,但还是容易增加无谓代码处理

IntersectionObserver

IntersectionObserver 对象,用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见

通过这个api我们不用再主动去监听元素位置了,在页面渲染一开始,通过这个api指明需要监听的元素,系统会自动去监听了元素位置。

let data = list;

<img class="img-{{index}}" wx:for="{{data}}"></img>

data.forEach((item,index)=>{
    this.createIntersectionObserver().relativeToViewport.observe(`.img-${index}`,res=>{
        if (res.intersectionRatio > 0){
            this.setData({
                item.imgShow:true
            })
        }
    })
})

存在图片太大而显示区域过小

这个问题就是指图片尺寸太大了,而页面上我们显示的尺寸又太小了,图片尺寸大,请求图片就越慢,导致页面渲染速度下降。

CDN图片处理

对于页面里面的图片,最好都把图片存储在cdn服务器上,一个是能充分利用cdn缓存来加快请求速度,另外一个就是cdn上能够将图片进行一定的处理,比如裁剪。我司就是通过cdn来响应图片处理,然后请求图片时告诉cdn服务器需要什么要的尺寸图片,由cdn服务器响应对应尺寸图片。

key值在列表渲染中的作用

key值在列表渲染的时候,能够提升列表渲染性能,为什么呢?首先得想想小程序的页面是如何渲染的,主要分为以下几步:

将wxml结构的文档构建成一个vdom虚拟数 页面有新的交互,产生新的vdom数,然后与旧数进行比较,看哪里有变化了,做对应的修改(删除、移动、更新值)等操作 最后再将vdom渲染成真实的页面结构

key值的作用就在第二步,当数据改变触发渲染层重新渲染的时候,会校正带有 key 的组件,框架会确保他们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。 key值如果不指明,默认会按数组的索引来处理,因而会导致一些类似input等输入框组件的值出现混乱的问题。

不加key,在数组末尾追加元素,之前已渲染的元素不会重新渲染。但如果是在头部或者中间插入元素,整个list被删除重新渲染,且input组件的值还出现了混乱,值没有正常被更新 添加key,在数组末尾、中间、或者头部插入元素,其它已存在的元素都不会被重新渲染,值也能正常被更新

因而,在做list渲染时,如果list的顺序发生变化时,最好增加key,且不要简单的使用数组索引当做key。