内存优化

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 中解绑监听
  // 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,如持续时间过长的 setTimeoutsetInterval,耗时较长的 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.offLocationChange(locationChangeListener)

5.3 未清理的定时器

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

资源加载优化

1. 控制图片资源的大小

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

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

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

图片对内存的影响

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

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

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

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

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

页面切换优化

页面切换的性能影响用户操作的连贯性和流畅度,是微信小程序运行时性能的一个重要组成部分。

1. 页面切换的流程

要想优化页面切换的性能,有必要先简单了解下微信小程序页面切换的过程。页面切换流程如图所示:

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

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

1.1 触发页面切换

页面切换的流程,从用户触发页面切换开始。

触发时间对应 PerformanceEntry(route) 中的 startTime。

页面切换可能由以下几类操作触发:

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

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

1.2 加载分包(若有)

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

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

1.3 视图层页面初始化

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

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

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

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

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

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

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

1.4 逻辑层页面初始化

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

基础库收到事件后会进行逻辑层的页面初始化,包括触发上一个页面的 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 中(作用于全局控制)

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

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

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

如下图所示,假设某微信小程序主页完整内容是分了三个 setData 进行的:

渲染性能优化

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

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

类似的,对于 <scroll-view><page-meta> 等可以通过 bindscroll 监听滑动事件的组件,也会存在这一情况。

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

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

Page({ 
  // ✅ 应直接不传入
})

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({
  data: {}
  userInfo: {} // 自定义数据
  currentUser: 'Wechat' // 自定义数据
  onTap() { }
  onLoad() {
    console.log(this.currentUser)
  }
})

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

  • ✅ 对于比较复杂的数据对象,建议在 Page onLoadComponent 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*/ ]
  }
})

合理使用 setData

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

1. setData 的流程

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

  • 逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer 等;
  • 将 data 从逻辑层传输到视图层;
  • 视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新。

2. 数据通信

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

iOS/iPadOS/MacOS 上,数据传输是通过 evaluateJavascript 实现的,还会有额外 JS 脚本解析和执行的耗时。

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

3. 使用建议

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

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

  • ✅ 页面或组件的 data 字段,应用来存放和页面或组件渲染相关的数据(即直接在 wxml 中出现的字段);
  • ✅ 页面或组件渲染间接相关的数据可以设置为「纯数据字段」,可以使用 setData 设置并使用 observers 监听变化;
  • ✅ 页面或组件渲染无关的数据,应挂在非 data 的字段下,如 this.userData = {userId: '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,例如倒计时更新。

4. 性能分析

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

运行时性能

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

在优化运行时性能前,建议开发者先了解下微信小程序的运行环境和运行机制。

开发者可以从以下方面着手进行启动性能的优化:

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

其他启动性能优化建议

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

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

1. 合理规划版本发布

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

  • 重新获取微信小程序的基础信息
  • 进行微信小程序代码包的增量更新
  • 重新生成 JS 代码的 Code Cache
  • 重新生成初始渲染缓存

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

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

回退和发布新版本对于启动耗时的影响是一致的。

首屏渲染优化

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

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

除了优化代码注入的耗时外,「按需注入」和「用时注入」也可以减少需要初始化的组件数量,降低实际页面渲染的耗时,使「首页渲染完成」提前。

启用「按需注入」之后,部分组件代码注入会被延迟到首页渲染阶段执行,导致阶段耗时上涨,但总耗时一般会下降。

2. 启用「初始渲染缓存」

自基础库版本 2.11.1 起,微信小程序支持启用初始渲染缓存。开启后,可以在非首次启动时,使视图层不需要等待逻辑层初始化完毕,而直接提前将页面渲染结果展示给用户,这可以使「首页渲染完成」和页面对用户可见的时间大大提前。

3. 避免引用未使用的自定义组件

在页面渲染时,会初始化在当前页面配置和全局配置通过 usingComponents 引用的自定义组件,以及组件所依赖的其他自定义组件。未使用的自定义组件会影响渲染耗时。

当组件不被使用时,应及时从 usingComponents 中移除。

4. 精简首屏数据

首页渲染的耗时与页面的复杂程度正相关。对于复杂页面,可以选择进行渐进式的渲染,根据页面内容优先级,优先展示页面的关键部分,对于非关键部分或者不可见的部分可以延迟更新。

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

5. 提前首屏数据请求

很多微信小程序在渲染首页时,需要依赖服务端的接口数据(如商品列表等),此时微信小程序的首页可能是空白或者骨架屏。

由于网络请求需要相对较长的时间,我们建议开发者在 Page.onLoad 或更早的时机发起网络请求,而不应等待 Page.onReady 之后再进行。

为了进一步提前请求发起的时机,微信小程序为开发者提供了以下能力:

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

6. 缓存请求数据

微信小程序提供了wx.setStorage、wx.getStorage等读写本地缓存的能力,数据存储在本地,返回的会比网络请求快。如果开发者基于某些原因无法采用数据预拉取与周期性更新,我们推荐优先从缓存中获取数据来渲染视图,等待网络请求返回后进行更新。

7. 骨架屏

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

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

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

代码注入优化

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

1. 使用按需注入

推荐所有微信小程序使用

通常情况下,在微信小程序启动时,启动页面依赖的所有代码包(主包、分包、插件包、扩展库等)的所有 JS 代码会全部合并注入,包括其他未访问的页面以及未用到自定义组件,同时所有页面和自定义组件的 JS 代码会被立刻执行。这造成很多没有使用的代码在微信小程序运行环境中注入执行,影响注入耗时和内存占用。

自基础库版本 2.11.1 起,可以通过开启「按需注入」特性避免不必要的代码注入和执行,以降低微信小程序的启动时间和运行时内存。

{
  "lazyCodeLoading": "requiredComponents"
}

注意:启用按需注入后,页面 JSON 配置中定义的所有组件和 app.jsonusingComponents 配置的全局自定义组件,都会被视为页面的依赖并进行注入和加载。建议开发者及时移除 JSON 中未使用自定义组件的声明,并尽量避免在全局声明使用率低的自定义组件,否则可能会影响按需注入的效果。

2. 使用用时注入

在打开上述「按需注入」特性的前提下,可以通过「用时注入」特性使一部分自定义组件不在启动时注入,而是在真正被渲染时才进行注入,进一步降低微信小程序的启动和首屏时间。

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

微信小程序启动流程中,会注入开发者代码并顺序同步执行 App.onLaunch, App.onShow, Page.onLoad, Page.onShow

在微信小程序初始化代码(Page,App 定义之外的内容)和上述启动相关的几个生命周期中,应尽量减少或不调用同步 API。绝大多数同步 API 会以 Sync 结尾,但有部分特例,比如 getSystemInfo

同步 API 虽然使用起来更简单,但是会阻塞当前 JS 线程,影响代码执行。如非必要,应尽可能的使用异步 API 代替同步,并将启动过程中非必要的同步 API 调用延迟到启动完成后进行。

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

3.1 getSystemInfo/getSystemInfoSync

由于历史原因,这两个接口都是同步实现。由于 getSystemInfo 接口里承载了过多内容,单次调用可能比较久。

如非必要,建议开发者对调用结果进行缓存,避免重复调用。启动过程中应尽可能最多调用一次

建议优先使用拆分后的 getSystemSetting/getAppAuthorizeSetting/getDeviceInfo/getWindowInfo/getAppBaseInfo 按需获取信息,或使用使用异步版本 getSystemInfoAsync。

3.2 getStorageSync/setStorageSync

getStorageSync/setStorageSync 应只用来进行数据的持久化存储,不应用于运行时的数据传递或全局状态管理。启动过程中过多的读写存储,也会显著影响微信小程序代码注入的耗时。

对于简单的数据共享,可以使用在 App 上增加全局数据对象完成:

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

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

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

在微信小程序初始化代码(Page,App 定义之外的内容)和启动相关的几个生命周期中,应避免执行复杂的运算逻辑。复杂运算也会阻塞当前 JS 线程,影响启动耗时。建议将复杂的运算延迟到启动完成后进行。

代码包体积优化

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

开发者可以采取以下手段优化代码包体积:

1. 合理使用分包加载

推荐所有微信小程序使用

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

分包加载具有以下优势:

  • 承载更多功能:微信小程序单个代码包的体积上限为 2M,使用分包可以提升微信小程序代码包总体积上限,承载更多的功能与服务。
  • 降低代码包下载耗时:使用分包后可以显著减少启动时需要下载的代码包大小,在不影响功能正常使用的前提下,有效降低启动耗时。
  • 降低微信小程序代码注入耗时:若未开启按需注入,微信小程序编译时会将所有 js 文件打包成同一个文件一次性的注入,并执行所有页面和自定义组件的代码。分包后可以降低注入和实际执行的代码量,从而降低注入耗时。
  • 降低页面渲染耗时:使用分包可以避免不必要的组件和页面初始化。
  • 降低内存占用:分包能够实现页面、组件和逻辑较粗粒度的按需加载,从而降低内存的占用。

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

1.1 独立分包

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

1.2 分包预下载

在使用「分包加载」后,虽然能够显著提升微信小程序的启动速度,但是当用户在使用微信小程序过程中跳转到分包内页面时,需要等待分包下载完成后才能进入页面,造成页面切换的延迟,影响微信小程序的使用体验。分包预下载便是为了解决首次进入分包页面时的延迟问题而设计的。

独立分包和分包预下载可以配合使用,获得更好的效果,详情请参考独立分包与分包预下载教程

1.3 分包异步化

「分包异步化」将微信小程序的分包从页面粒度细化到组件甚至文件粒度。这使得本来只能放在主包内页面的部分插件、组件和代码逻辑可以剥离到分包中,并在运行时异步加载,从而进一步降低启动所需的包大小和代码量。

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

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

app.json 中通过 usingComponents 全局引用的自定义组件和通过 plugins 全局引入的插件,会在微信小程序启动时随主包一起下载和注入 JS 代码,影响启动耗时。

即使扩展库和部分官方插件不占用主包大小,但是启动时仍然需要下载和注入 JS 代码,对启动耗时的影响和其他插件并没有区别。

  • 如果自定义组件只在某个分包的页面中使用,应定义在页面的配置文件中
    • 全局引入的自定义组件会被认为是所有分包、所有页面都需要的,会影响「按需注入」的效果和微信小程序代码注入的耗时。
  • 如果插件只在某个分包的中使用,请仅在分包中引用插件
    • 例如:很多微信小程序会用到「微信小程序直播」插件,但是直播功能通常不在主包页面中使用或较为低频,此时建议通过分包引入「微信小程序直播」插件。
    • 如果确实需要在主包中或被多个分包使用的插件,仍可以考虑将插件置于一个分包,并通过「分包异步化」的形式异步引入