首页 » 前端 » 正文

微信小程序进行时优化

小程序的运行时性能直接决定了用户在使用小程序功能时的体验。如果运行时性能出现问题,很容易出现页面滚动卡顿、响应延迟等问题,影响用户使用。如果内存占用过高,还会出现黑屏、闪退等问题。

在优化运行时性能前,建议开发者先了解下小程序的运行环境和运行机制。 开发者可以从以下方面着手进行启动性能的优化:

  • 合理使用 setData
  • 渲染性能优化
  • 页面切换优化
  • 资源加载优化
  • 内存优化

一 合理使用 setData

setData是小程序开发中使用最频繁、也是最容易引发性能问题的接口。

setData的过程,大致可以分成几个阶段:

  • 第一阶段:逻辑层虚拟DOM树的遍历和更新,触发组件生命周期和observer等;
  • 第二阶段:将data 从逻辑层传输到视图层;
  • 第三阶段·视图是虑州DOM树的更新 直实DOM元麦的更新共触发页面溶染更新

1、setData优化

  • data只包含渲染数据
  • 空置setData的频率
  • 控制后台页面的调用

2、数据通信

对于第2步,由于小程序的逻辑层和视图层是两个独立的运行环境、分属不同的线程或进程,不能直接进行数据共享,需要进行数据的序列化、跨线程/进程的数据传输、数据的反序列化,因此数据传输过程是异步的、非实时的。

iOS/iPadOS/MacOS上,数据传输是通过evaluateJavascript实现的,还会有额外JS脚本解析和执行的耗时。 数据传输的耗时与数据量的大小正相关,如果对端线程处于繁忙状态,数据会在消息队列中等待。

3、使用建议

3.1 data 应只包括渲染相关的数据

setData应只用来进行渲染相关的数据更新。用setData的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。

  • 页面或组件的data字段,应用来存放和页面或组件渲染相关的数据(即直接在wxml中出现的字段);
  • 页面或组件渲染间接相关的数据可以设置为「纯数据字段」,可以使用setData 设置并使用observers监听变化;页面或组件渲染无关的数据,应挂在非data的字段下,如 this.userData={userld: 'xxx'};
  • 避免在data 中包含渲染无关的业务数据;
  • 避免使用data 在页面或组件方法间进行数据共享;
  • 避免滥用 纯数据字段 来保存可以使用非 data字段保存的数据。

3.2控制setData频率

每次 setData 都会触发逻辑层虚拟DOM树的遍历和更新,也可能会导致触发一次完整的页面渲染流程。过于频繁(毫秒级)的调用setData,会导致以下后果:逻辑层JS线程持续繁忙,无法正常响应用户操作的事件,也无法正常完成页面切换;

视图层JS线程持续处于忙碌状态,逻辑层—>视图层通信耗时上升,视图层收到消息的延时较高,渲染出现明显延迟;

视图层无法及时响应用户操作,用户滑动贝面时感到明显卡顿,操作反馈延迟,用户操作事件无法及时传递到逻辑层,逻辑层办无法及时将操作处理结果及时传递到视图层。

因此,开发者在调用setData 时要注意:

  • 仅在需要进行页面内容更新时调用 setData;对连续的setData 调用尽可能的进行合并;避免不必要的setData;
  • 避免以过高的频率持续调用setData,例如毫秒级的倒计时;
  • 避免在onPageScroll 回调中每次都调用 setData。

3.3 选择合适的 setData 范围

组件的setData 只会引起当前组件和子组件的更新,可以降低虚拟DOM更新时的计算开销。

  • 对于需要频繁更新的页面元素(例如:秒杀倒计时),可以封装为独立的组件,在组件内进行 setData 操作。必要时可以使用CSS contain属性限制计算布局、样式和绘制等的范围。

3.4 setData应只传发生变化的数据

setData的数据量会影响数据拷贝和数据通讯的耗时,增加页面更新的开销,造成页面更新延迟。

  • setData应只传入发生变化的字段;
  • 建议以数据路径形式改变数组中的某一项或对象的某个属性,如 this.setData({'array[2].message': 'newVal', 'a.b.c.d': 'newVal'}),而不是每次都更新整个对象或数组;
  • 不要在setData 中偷懒一次性传所有data:this.setData(this.data)。

3.5控制后台态页面的setData

由于小程序逻辑层是单线程运行的,后台态页面去 setData也会抢占前台页面的运行资源,且后台态页面的的渲染用户是无法感知的,会产生浪费。在某些平台上,小程序渲染层各WebView也是共享同一个线程,后台页面的渲染和逻辑执行也会导致前台页面的卡顿。

  • 页面切后台后的更新操作,应尽量避免,或延迟到页面onShow 后延迟进行;
  • 避免在切后台后仍进行高频的setData,例如倒计时更新。

二 渲染性能优化

1.适当监听页面或组件的scroll事件

只要用户在Page构造时传入了onPageScroll 监听,基础库就会认为开发者需要监听页面scoll事件。此时,当用户滑动页面时,事件会以很高的频率从视图层发送到逻辑层,存在一定的通信开销。

类似的,对于、等可以通过bindscroll 监听滑动事件的组件,也会存在这一情况。正是由于scroll事件触发的频率很高,因此开发者很容易误用,在使用时需要注意:

  • 非必要不监听scroll事件;
  • 在实现与滚动相关的动画时,优先考虑滚动驱动动画(仅)或WXS响应事件
  • 不需要监听事件时,Page 构造时应不传入 onPageScroll 函数,而不是留空函数;
  • 避免在scroll事件监听函数中执行复杂逻辑;
  • 避免在scroll事件监听中频繁调用 setData或同步API。

2.选择高性能的动画实现方式

  • 优先使用CSS渐变、CSS动画、或小程序框架提供的其他动画实现方式完成动画;
  • 在一些复杂场景下,如果上述方式不能满足,可以使用WXS响应事件 动态调整节点的style属性做到动画效果。同时,这种方式也可以根据用户的触摸事件来动态地生成动画;
  • 避免通过连续 setData 改变界面的形式来实现动画。虽然实现起来简单灵活,但是极易出现较大的延迟或卡顿,甚至导致小程序僵死;
  • 如果不得不采用setData方式,应尽可能将页面的setData改为自定义组件中的setData 来提升性能。

3.使用IntersectionObserver 监听元素曝光

部分业务场景会需要监控元素曝光情况,用于进行一些页面状态的变更或上报分析。

  • 建议使用节点布局相交状态监听 IntersectionObserver 推断某些节点是否可见、有多大比例可见;
  • 避免通过监听 onPageScroll事件,并在回调中通过持续查询节点信息 SelectQuery来判断元素是否可见。

4.控制WXML节点数量和层级

一个太大的WXML节点树会增加内存的使用,样式重排时间也会更长,影响体验。

  • 建议一个页面WXML节点数量应少于1000个,节点树深度少于30层,子节点数不大于60个。

5.控制在Page构造时传入的自定义数据量

为了便于开发,开发者可以添加任意的函数或数据到Page构造传入的Object 参数中,并在页面的函数内用this 访问。

为了保证自定义数据在不同的页面实例中也是不同的实例,小程序框架会在页面创建时对这部分数据(函数类型字段除外)做一次深拷贝,如果自定义数据过多或过于复杂,可能带来很大的开销。

对于比较复杂的数据对象,建议在Page onLoad或Component created 时手动赋值到 this上,而不是通过Page构造时的参数传入。使用复杂对象作为自定义数据。运行时手动赋值到this。开发者可以根据需要选择进行深拷贝、浅拷贝或不拷贝。

三 页面切换优化

开发者可以通过wx.getPerformance接口中 entryType为navigationI name为route的指标(PerformanceEntry),获取页面切换耗时时。

当切换的目标页面已加载完成时(例如:路由类型为navigateBack,或switchTab 到一个已加载到页面),不需要进行「视图层页面初始化」和「目标页面渲染」,「逻辑层页面初始化」也会较为简化。

1、触发页面切换

1.1页面切换的流程。

从用户触发页面切换开始。触发时间对应 PerformanceEntry(route)中的startTime。 页面切换可能由以下几类操作触发:

  • 小程序API调用:开发者根据用户操作,调用 wx.navigateTo、wx.navigateBack、wx.redirectTo、wx.reLaunch、wx.switchTab等API。
  • 用户点击组件进行页面切换。
  • 用户点击原生UI触发:例如点击tabBar(自定义tabBar除外)、点击左上角「返回首页」按钮、点击系统返回键或左滑返回等。
  • 小程序热启动时自动 reLauch:小程序热启动的B类场景。

目前后两种情况暂时未能获取准确的触发时间,PerformanceEntry(route)的startTime和navigationStart一致。

1.2加载分包(若有)

如果页面切换的目标页面在分包中,页面切换时需要下载分包,并在逻辑层注入执行分包内的JS代码。

小程序生命周期内,每个分包只会在逻辑层注入一次。

1.3视图层页面初始化

小程序视图层的每个页面都是由独立的WebView 渲染的,因此页面切换时需要一个新的WebView环境。视图层页面初始化主要会做以下事情:

  • 创建 WebView
  • 注入视图层的小程序基础库
  • 注入主包的公共代码(独立分包除外)
  • (若页面位于分包中)注入分包的公共代码
  • 注入页面代码

为了降低视图层页面初始化的耗时,在页面渲染完成后,通常会进行必要的预加载供页面切换时使用。预加载主要会做以下事情:

  • 创建 WebView
  • 注入主包的公共代码(若主包已在本地)
  • 注入视图层的小程序基础库

如果页面切换过快,或预加载的环境被回收,则需要在页面切换时重新创建环境。

如果页面切换时有预加载好的环境,可以大大降低页面切换的耗时。

当切换的目标页面已加载完成时,不需要进行本阶段。

1.4.逻辑层页面初始化

完成分包加载和WebView 创建后,客户端会向基础库派发路由事件。I

基础库收到事件后会进行逻辑层的页面初始化,包括触发上一个页面的onHide/onUnload、页面组件树初始化、更新页面栈并生成初始数据发送到视图层,并依次触发目标的onLoad,onShow生命周期。如果启用了「按需注入」,这一阶段还会注入页面代码。

基础库收到事件的时间对应PerformanceEntry(route)中的navigationStart,对应PerformanceEntry(firstRender)的开始时间。 当切换的目标页面已加载完成时,不需进行页面组件树初始化和初始数据的发送,且不会触发目标页面的onLoad。

1.5目标页面渲染

页面切换的目标页面不存在时,会触发页面的首次渲染。

在完成视图层代码注入,并收到逻辑层发送的初始数据后,结合从初始数据和视图层得到的页面结构和样式信息,小程序框架会进行页面渲染,并触发页面的onReady事件。视图层渲染完成,触发页面onReady事件的时间,对应PerformanceEntry(firstRender)的结束时间。

当切换的目标页面已加载完成时,不需要进行本阶段。1.6页面切换动画

页面渲染完成后,客户端会进行页面切换的动画(如:从右向左推入页面)。如果页面初始化和渲染的时间超过固定时间,为避免用户以为页面无响应,页面会提前推入。页面推入动画完成的时间,对应PerformanceEntry(route)的结束时间

2、如何优化页面切换

2.1 避免在onHide/onUnload 执行耗时操作

页面切换时,会先调用前一个页面的onHide或onUnload生命周期,然后再进行新页面的创建和渲染。如果onHide和onUnload 执行过久,可能导致页面切换的延迟。

  • onHide/onUnload 中的逻辑应尽量简单,若必须要进行部分复杂逻辑,可以考虑用setTimeout 延迟进行。
  • 减少或避免在onHide/onUnload 中执行耗时逻辑,如同步接口调用、setData等。

2.2首屏渲染优化

页面首屏渲染是页面切换耗时的重要组成部分,优化手段可以参考启动性能优化中首屏渲染优化部分

2.3提前发起数据请求

在一些对性能要求比较高的场景下,当使用JSAPI进行页面跳转时(例如 wx.navigateTo),可以提前为下一个页面做一些准备工作。页面之间可以通过EventChannel 进行通信。

例如,在页面跳转时,可以同时发起下一个页面的数据请求,而不需要等到页面onLoad时再进行,从而可以让用户更早的看到页面内容。尤其是在跳转到分包页面时,从发起页面跳转到页面onLoad之间可能有较长的时间间隔,可以加以利用。

2.4控制预加载下个页面的时机

基础库2.15.0开始支持,仅安卓。低版本配置不生效。

如1.3节所述,小程序页面加载完成后,会预加载下一个页面。默认情况下,小程序框架会在当前页面onReady触发200ms后触发预加载。

在安卓上,小程序渲染层所有页面的WebView 共享同一个线程。很多情况下,小程序的初始数据只包括了页面的大致框架,并不是完整的内容。页面主体部分需要依靠setData 进行更新。因此,预加载下一个页面可能会阻塞当前页面的渲染,造成setData和用户交互出现延迟,影响用户看到页面完整内容的时机。

为了让用户能够更早看到完整的页面内容,避免预加载流程对页面加载过程的影响,开发者可以配置 handleWebviewPreload 选项,来控制预加载下个页面的时机。handleWebviewPreload 有以下取值

  • static:默认值。在当前页面 onReady触发200ms后触发预加载
  • auto:渲染线程空闲时进行预加载。由基础库根据一段时间内 requestAnimationFrame的触发频率算法判断。
  • manual:由开发者通过调用 wx.preloadWebview 触发。开发者可以在页面主要内容的setData 结束后手动触发。例如:

在app.json中(作用于全局控制),或在页面JSON 文件中(只作用于单个页面)

四、资源加载优化

1、控制图片资源的大小

开发者应根据功能需要和实际显示区域的大小,选择合适的图片尺寸、图片格式和压缩比。

图片体积太大,可能导致下列后果

  • 增加图片下载时间,导致用户看到图片时机延迟;
  • 对用户造成非必要的流量消耗;
  • 影响图片解码和绘制的耗时,可能更容易造成掉帧、卡顿或白屏,甚至无法正常进行滚动和页面切换(低端设备上会尤为明显) ;
  • 内存占用增长,尤其是大图片和长列表中的大量图片会导致内存占用急剧上升。

图片对内存的影响:

iOS系统内存紧张时,会主动回收掉一部分WebView。大图片和长列表中的大量图片很容易引起系统对WebView的回收,导致小程序白屏,严重时会触发微信强制关闭小程序。

内存增长如果超过了限制,也会导致小程序出现白屏或黑屏,甚至整个小程序发生闪退。

2、避免滥用image组件的widthFix/heightFix模式

widthFix/heightFix模式会在图片加载完成后,动态改变图片的高度或宽度。图片高度或宽度的动态改变,可能会引起页面内大范围的布局重排,导致页面发生抖动,并造成卡1顿。

对于页面的背景图或banner图,应尽量预先指定图片的尺寸,避免图片加载完成后再进行二次的尺寸调整。

五、内存优化

1、合理使用分包加载

使用分包加载不仅能优化启动耗时,也能够实现页面、组件和逻辑较粗粒度的按需加载,从而降低内存的占用。

2、使用按需注入和用时注入

通过开启「按需注入」和「用时注入」,可以在运行时避免加载未使用到的页面和组件,降低运行时的内存占用。

3、内存分析

如果要更精细的分析小程序逻辑层的内存分布情况,可以使用开发者工具调试器的「内存调试」或「真机调试2.0」提供的「内存调试」能力。

4、处理内存告警

当小程序占用系统资源过高,可能会被系统销毁或被微信客户端主动回收。在iOS上,当微信客户端在一定时间间隔内连续收到系统内存告警时,会根据一定的策略,主动销毁小程序,并提示用户「运行内存不足,请重新打开该小程序」。

建议小程序在必要时使用 wx.onMemoryWarning 监听内存告警事件,进行必要的内存清理。例如:释放一些暂时不用的组件或JS对象。

5、小程序常见的内存泄露问题

存在内存泄露问题会导致小程序在运行过程中内存占用持续增长,引起小程序闪退或被被微信强制销毁。

5.1小程序长期持有页面实例,导致页面实例和引用的组件无法正常销毁

页面unload之后,基础库会从页面栈中将页面实例清理。正常情况下,JS垃圾回收机制会将页面进行回收,释放内存。

但如果开发者代码中持有的页面实例(this)未释放,则会导致页面未被正常回收,引起内存泄露。建议开发者注意,并在unload中进行必要的清理。

案例一:页面实例被未解绑的事件监听引用

事件监听器中持有了页面的this,如果页面销毁后监听未解绑,会导致页面无法释放。代码块

Page({
    themeChangeHandler({theme }){
        this.setData({ theme})
    }, 

onLoad(){
    this._handler =this.themeChangeHandler.bind(this)
    wx.onThemeChange(this._handler) 
},

//修复方法:unload 中解绑监听10
//onUnload() { 
//    wx.offThemeChange(this.handler)
//},
})

案例二:页面实例被页面外变量或全局变量引用

函数闭包内持有了页面的this,且函数被挂到全局或页面声明周期外的变量,会导致页面无法释放。代码块

let languageListener = null
Page({
    onLoad(){
    getApp().userInfoChangeListener = ({ userName }) =>{ 
        this.setData({ userName }) {
            languageListener = ({ lang }) =>{ this.setData({lang })
        }  
},
//修复方法:unload 中进行清理13
// onUnload(){
//    getApp().userInfoChangeListener = null
//    languageListener =null
//}, 
})

案例三:页面实例被异步回调长时间引用

如果在长时间未返回的异步回调中访问了页面的this,如持续时间过长的setTimeout、setlnterval,耗时较长的wxAPI回调(如长时间的wx.request等),会导致页面无法释放。

修复方法就是在onUnload的时候清理掉

5.2事件监听未及时解绑

事件监听结束后,应及时解绑监听器代码块

const locationChangeListener = function (res) { 
    console.1og('location change', res) 
} 
wx.onLocationChange(locationChangeListener) 
wx.startLocationUpdate()
//监听结束后
wx.stopLocationUpdate()
//修复方法:不使用后及时解绑监听
//wx.offLocationChange(locationChangeListener) 

5.3未清理的定时器

开发者在开发如「秒杀倒计时」等功能时,可能会使用 setlnterval 设置定时器,页面或组件销毁前,需要调用clearlnterval方法取消定时器。