微信小程序性能优化实践

利用微信开发者工具 ---> 对项目进行诊断

分析工具菜单选项:

代码依赖分析

性能报告

代码质量扫描

调试区的Performance面板

Memory面板

JavaScript Profiler面板

体验评分(Audits)面板

参考官方文档性能与体验

一、启动性能

小程序启动是小程序用户体验中极为重要的一环,启动耗时过长会造成小程序用户流失,影响用户体验。

代码包体积优化

启动性能优化最直接的手段是降低代码包大小,代码包大小直接影响了下载耗时,影响用户启动小程序时体验。

1.合理使用分包加载

分包加载是使用优化小程序启动耗时效果最明显的手段。建议开发者按照功能划分,将小程序的页面按使用频率和场景拆分成不同分包,实现代码包的按需加载。

结合分包加载的几个扩展功能可以进一步优化启动耗时:

1.1 独立分包

1.2 分包预下载

1.3 分包异步化(能有效解决主包大小过度膨胀的问题)

2. 避免非必要的全局自定义组件和插件

2. 控制代码包内的资源文件

2. 及时清理无用的代码(微信开发者工具提供的 【代码静态依赖分析】)

代码注入优化

小程序代码注入的优化可以从优化代码量优化执行耗时两个角度着手

  1. 使用按需注入


自基础库版本 2.11.1 起,可以通过开启「按需注入」特性避免不必要的代码注入和执行,以降低小程序的启动时间和运行时内存。
{
  "lazyCodeLoading": "requiredComponents"
}
  1. 使用用时注入

  1. 启动过程中减少同步API的调用

常见的开发者容易在启动时过于频繁调用的 API 有:

3.1 getSystemInfo/getSystemInfoSync

3.2 getStorageSync/setStorageSync (应只用来进行数据的持久化存储,不应用于运行时的数据传递或全局状态管)对于简单的数据共享,可以使用在 App 上增加全局数据对象完成:


// app.js
App({
  globalData: { // 全局共享的数据
    userName: 'Wechat'
  }
})

// pages/index.js
const app = getApp()
Page({
  onLoad() {
    const { userName } = app.globalData
  }
})

4. 避免启动过程进行复杂运算

首屏渲染优化

页面首屏渲染的优化,目的是让「首页渲染完成」(Page.onReady) 尽可能提前。但很多情况下「首页渲染完 成」可能还是空白页面,因此更重要的是让用户能够更早的看到页面内容(First Paint 或 First ContentfulPaint)。

  1. 使用「按需注入」和「用时注入

  1. 启用「初始渲染缓存

  1. 避免应用未使用的自定义组件

  1. 精简首屏数据

首页渲染的耗时与页面的复杂程度正相关。此外,与视图层渲染无关的数据应尽量不要放在 data 中,避免影响页面渲染时间。

  1. 提前首屏数据请求

很多小程序在渲染首页时,需要依赖服务端的接口数据(如商品列表等),此时小程序的首页可能是空白或者骨架屏。为了进一步提前请求发起的时机,小程序为开发者提供了以下能力:

  • 数据预拉取:能够在小程序冷启动时,由微信客户端通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间。

  • 周期性更新:在用户未打开小程序的情况下,也能从服务器提前拉取数据,当用户打开小程序时可以更快地渲染页面,减少用户等待时间。

  1. 缓存请求数据

  1. 骨架屏

骨架屏通常用于在页面完全渲染之前,通过一些灰色的区块大致勾勒出轮廓,待数据加载完成后,再替换成真实的内容。

建议开发者在页面数据未准备好时(例如需要通过网络获取),尽量避免展示空白页面,而是先通过骨架屏展示页面的大致结构,请求数据返回后再进行页面更新。以提升用户的等待意愿。

开发者工具提供了生成骨架屏的能力,帮助开发者更便捷的维护骨架屏。

其他优化建议

除了 代码包体积代码注入首屏渲染之外,发版频率等因素也会影响小程序启动耗时。

针对这些因素,我们建议开发者:

1. 合理规划版本发布

小程序启动时如果检测到版本更新(具体策略请参考小程序更新机制),会进行以下操作,影响启动耗时

  • 重新获取小程序的基础信息

  • 进行小程序代码包的增量更新

  • 重新生成 JS 代码的 Code Cache

  • 重新生成初始渲染缓存

能够快速迭代发布是小程序相对 APP 的一个优势,但是过于频繁的新版本发布可能会导致部分用户每次使用都需要进行小程序的更新,导致平均启动耗时变长。

在不影响小程序正常功能迭代的前提下,我们建议开发者提前做好版本规划,控制版本发布的频率。

二、运行时性能

合理使用setData

1. setData 的流程, 大致分成几个阶段:

- 逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer 等;

- 将 data 从逻辑层传输到视图层;

- 视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新。

2. 数据通信,数据传输的耗时与数据量的大小正相关,如果对端线程处于繁忙状态,数据会在消息队列中等待

  1. 使用建议

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

3.2 控制 setData 的频率,每次 setData 都会触发逻辑层虚拟 DOM 树的遍历和更新,也可能会导致触发一次完整的页面渲染流程。因此,开发者在调用 setData 时要注意:

  • ✅ 仅在需要进行页面内容更新时调用 setData;

  • ✅ 对连续的 setData 调用尽可能的进行合并

  • ❌ 避免不必要的 setData;

  • ❌ 避免以过高的频率持续调用 setData,例如毫秒级的倒计时;

  • ❌ 避免在 onPageScroll 回调中每次都调用 setData。

3.3 选择合适的 setData 范围

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

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

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

3.5 控制后台态页面的 setData

  • ✅ 页面切后台后的更新操作,应尽量避免,或延迟到页面 onShow 后延迟进行;

  • ❌ 避免在切后台后仍进行高频的 setData,例如倒计时更新。

  1. 开发者可以通过组件的 setUpdatePerformanceListener 接口获取更新性能统计信息,来分析产生性能瓶颈的组件。

渲染性能优化

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

只要用户在 Page 构造时传入了 onPageScroll 监听,基础库就会认为开发者需要监听页面 scoll 事件。

正是由于 scroll 事件触发的频率很高,因此开发者很容易误用,在使用时需要注意:

  • ✅ 非必要不监听 scroll 事件;

  • ❌ 不需要监听事件时,Page 构造时应不传入 onPageScroll 函数,而不是留空函数;

  • ❌ 避免在 scroll 事件监听函数中执行复杂逻辑;

  • ❌ 避免在 scroll 事件监听中频繁调用 setData 或同步 API

  • Page({ onPageScroll () {} // ❌不要保留空函数 }) Page({ // ✅ 应直接不传入 })

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

  1. 使用 IntersectionObserver 监听元素曝光

  1. 控制wxml节点数量和层级

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

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

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

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


Page({
  data: {}
  userInfo: {} // 自定义数据
  currentUser: 'Wechat' // 自定义数据
  onTap() { }
  onLoad() {
    console.log(this.currentUser)
  }
})

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

  • ✅ 对于比较复杂的数据对象,建议在 Page onLoad 或 Component created 时手动赋值到 this 上,而不是通过 Page 构造时的参数传入。


// ❌ 使用复杂对象作为自定义数据
Page({
  onLoad() { }
  bigData: { /* A complex object */ },
  longList: [ /* A long complex array*/ ]
})

// ✅ 运行时手动赋值到 this。开发者可以根据需要选择进行深拷贝、浅拷贝或不拷贝。
Page({
  onLoad() {
    this.bigData = { /* A complex object */ },
    this.longList = [ /* A long complex array*/ ]
  }
})

页面切换优化

  1. 如何优化页面切花

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

2.2 首屏渲染优化(优化手段可以参考启动性能优化中首屏渲染优化部分)

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

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 中(作用于全局控制)

{
  "window": {
    "handleWebviewPreload": "auto"
  }
}

或在页面 JSON 文件中(只作用于单个页面)

{
  "handleWebviewPreload": "manual"
}
Page({
  onLoad() {
    this.setData({
      fullData: {}
    }, () => {
      // 只有配置为 manual 时需要调用
      wx.preloadWebview?.()
    })
  }
})

资源加载优化

  1. 控制图片资源大小

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

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

  • 增加图片下载时间,导致用户看到图片时机延迟;

  • 对用户造成非必要的流量消耗;

  • 影响图片解码和绘制的耗时,可能更容易造成掉帧、卡顿或白屏,甚至无法正常进行滚动和页面切换(低端设备上会尤为明显);

  • 内存占用增长,尤其是大图片和长列表中的大量图片会导致内存占用急剧上升。

图片对内存的影响

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

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

  1. 避免滥用 image组件的 widthFix / heightFix

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

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

内存优化

1. 合理使用分包加载

1. 使用按需注入和用时注入(情请参考《启动优化 - 代码注入优化》

  1. 内存分析(可以使用开发者工具调试器的「内存调试」或「真机调试 2.0」提供的「内存调试」能力。)

  1. 处理内存告警

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

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

  1. 小程序常见的内存泄漏问题

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 中解绑监听
  // onUnload() {
  //   wx.offThemeChange(this._handler)
  // },
})

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

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


let languageListener = null

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

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

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


Page({
  onLoad() {
    this._timer = setInterval(() => {
      this.setData({
        timerValue: Date.now()
      })
    }, 1000)
  },
  // 修复方法:unload 中进行清理
  // onUnload() {
  //   clearInterval(this._timer)
  // },
})

5.2 时间监听未及时解绑

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


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

5.3 未清理的定时器

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值