小程序性能优化技巧

5,056 阅读9分钟

网页性能优化是前端一个老生常谈的话题,但微信小程序因为双线程的架构设计,跟传统 Web 页面不太一样。所以,今天来探究下微信小程序内的性能优化问题。

首先,要问大家一个问题:从打开微信小程序到首页展示在大家面前,要经过哪些过程?(可以类比与前端另一个常见问题:Web 页面从输入 url 到页面展示具体经过了哪些过程)

1. 启动过程

相信大家对 Web 页面的展现过程非常清楚。那么小程序呢,简要地说,小程序要历经下面几个启动过程:

小程序启动过程

  1. 小程序初始化: 微信初始化小程序环境:包括 Js 引擎和 WebView 进行初始化,并注入公共基础库。 这步是微信做的,在用户打开小程序之前就已经准备好了,是小程序运行环境预加载。

  2. 下载小程序代码包 对小程序业务代码包进行下载:下载的不是小程序的源代码,而是编译、压缩、打包之后的代码。

  3. 加载小程序代码包 对下载完成对代码包进行注入执行。 此时,app.js、页面所在的 Js 文件和所有其他被 requireJs 文件会被自动执行一次,小程序基础库会完成所有页面的注册。

  4. 初始化小程序首页 拉取数据,从逻辑层传递到视图层,进行渲染。

2. 性能优化

既然清楚了小程序的启动过程,那我们就可以针对其中的每一个环节进行性能分析和优化。

2.1 小程序初始化环节

由于这个环节是微信执行的,属于小程序底层的执行耗时,所以我们开发者无法操控。已知的是:iOS 初始化比 Android 快一些。

2.2 下载和加载环节

一般来说:下载环节是耗时比较长的环节。 对低于 1MB 的代码包,其下载时间可以控制在 929ms(iOS)、1500ms(Android)内。 那么提升下载性能最关键的一点是:控制包的大小。

常见的控制代码包大小的方法如下:

  • 精简代码,清除无用代码
  • 减少在代码包中直接嵌入的资源文件
  • 图片放在cdn,使用适当的图片格式

如果小程序比较复杂,优化后的代码总量仍然较大,此时可以采用分包加载的方式进行优化。

其原理是: 一般情况下,小程序的代码将打包在一起,在小程序启动时一次性下载完成。 采用分包时,小程序的代码包可以被划分为几个:一个是“主包”,包含小程序启动时会马上打开的页面代码和相关资源;其余是“分包”,包含其余的代码和资源。 这样,小程序启动时,只需要先将主包下载完成,就可以立刻启动小程序。这样就可以显著降低小程序代码包的下载时间。

分包

但是这个时候又出现了另一个问题,在我们访问分包页面时,需要先下载完分包代码,才能打开分包页面,这是能感觉到到明显卡顿,体验也是比较差的。

分包启动

我们可以通过配置 preloadRule 进行分包预加载:打开首页,加载完主包后,静默加载其他分包。

分包预加载

除了普通分包方案,小程序还有独立分包的方案。 独立分包是小程序中一种特殊类型的分包,可以独立于主包和其他分包运行。从独立分包中页面进入小程序时,不需要下载主包。当用户进入普通分包或主包内页面时,主包才会被下载。 我们可以把它用于一些比较独立的页面,比如活动页等。

普通分包启动

独立分包启动

2.3 初始化首页环节

到了首屏渲染这个环节,有以下的几个优化建议:

  1. 提前请求:在页面 onLoad 阶段就可以发起异步请求,不用等页面 ready。如果能在前置页面点击跳转时预请求当前页的核心异步请求,效果会更好;

  2. 善用缓存:对一些变动频率很低的异步数据进行缓存,下次启动时可以直接利用;

  3. 优化交互:在首屏渲染的期间,利用 loading 效果或展示骨架图,来缓解用户等待的焦虑。

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

初始渲染完毕后,视图层可以在开发者调用setData后执行界面更新。

2.4 setData 优化

2.4.1 setData 的工作原理

与传统的浏览器 Web 页面最大区别在于,小程序的是基于 双线程 模型: 在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。

两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 Native 的 JSBrigde 做中转。

双线程模型

小程序通过 setData 更新数据到视图改变,完整的流程如下:

  1. 调用 setData 方法;
  2. 逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,将待传输数据转换成字符串并拼接到特定的JS脚本, 并通过 evaluateJavascript 执行脚本将数据传输到渲染层。
  3. 渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染。
  4. WebView 线程开始执行渲染时,将 datasetData 数据套用在WXML 片段上,得到一个新节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。最后,将 setData 数据合并到 data 中,并用新节点树替换旧节点树,用于下一次重渲染。

2.4.2 setData 注意事项

如上文所说:一次 setData 带来两次开销:通信的开销 + WebView 更新的开销。 setData 是小程序开发使用最频繁的 API 之一,也是最容易引发性能问题的。

所以使用时需要注意以下几点:

  1. 与界面渲染无关的数据最好不要设置在 data 中,可以考虑设置在 page 对象的其他字段下;

    this.setData({
        a: '与渲染有关的字符串',
        b: '与渲染无关的字符串'
    })
     
    // 可以优化为
    this.setData({
        a: '与渲染有关的字符串',
     })
    this.b = '与渲染无关的字符串'
    
  2. 不要过于频繁调用 setData,应考虑将多次 setData 合并成一次 setData 调用;

    this.setData({ a: 1 })
    this.setData({ b: 2 })
    // 可优化为
    this.setData({ a: 1, b: 2 })
    

    当需要在频繁触发的用户事件(如 PageScroll 、 Resize 事件)中调用 setData ,合理的利用函数防抖(debounce) 和 函数节流(throttle) 。

    还可以自己设计一个 diff 算法,重新对 setData 进行封装,使得在 setData 执行之前,让待更新的数据与原 data 数据做 diff对比,如果一样则跳过执行更新。不少小程序框架都有其类似的封装。

  3. 列表局部更新 在更新列表的某一个数据时。不要用 setData 进行全部数据的刷新。查找对应 id 的那条数据的下标(index是不会改变的),用 setData 进行局部刷新。

    this.setData({
        `list[${index}]` = newList[index]
    })
    
  4. 合理使用小程序组件 自定义组件的更新只在组件内部进行,不会影响页面其他元素。因为各个组件具有独立的逻辑空间、数据、样式环境及 setData 调用。 基于自定义组件的 Shadow DOM 模型设计,我们可以将页面中一些需要高频执行 setData 更新的功能模块(如倒计时、进度条等)封装成自定义组件嵌入到页面中。 当这些自定义组件视图需要更新时,执行的是组件自己的 setData,新旧节点树的对比计算和渲染树的更新都只限于组件内有限的节点数量,有效降低渲染时间开销。

    自定义组件

    当然,并不是使用自定义组件越多会越好,页面每新增一个自定义组件, Exparser 需要多管理一个组件实例,内存消耗会更大。因此要合理的使用自定义组件,同时页面设计也要注意不滥用标签。

3. 分析工具

其实性能优化最重要的是拿数据说话。但是现在小程序里没有完整成熟的定量的性能评测标准,目前有以下的分析工具可供参考:

  1. 性能 Trace 工具 微信 Andoid 6.5.10 开始,提供了 Trace 导出工具,开发者可以在开发者工具 Trace Panel 中使用该功能。 使用教程: developers.weixin.qq.com/miniprogram…

  2. 性能面板 从微信 6.5.8 开始,提供了性能面板让开发者了解小程序的性能。开发者可以在开发版小程序下打开性能面板。 打开方法:进入开发版小程序,进入右上角更多按钮,点击 显示性能窗口

  3. 加载性能监控 在小程序后台,我们可以看到加载性能监控。指标有三个:

  • 启动总耗时

    总启动耗时

  • 下载耗时

    下载耗时

  • 初次渲染耗时

    初次渲染耗时

总启动耗时 = 下载耗时 + 初次渲染耗时 + 其他耗时。

优化后可根据以上几个工具进行数据对比,来判断优化的效果。

主要参考来源: