优化建议

运行原理

与传统的h5应用不同,小程序的运行从架构上分为 webviewworker 两个部分。webview 负责渲染,worker 则负责存储 数据 和执行 业务逻辑

  • webviewworker 之间的通信是异步的。这意味着当我们调用 setData 时,我们的 数据 并不会立即得到渲染,而是需要从 worker 异步传输到 webview
  • 数据传输时需要序列化为字符串,然后通过 evaluateJavascript 的方式传输。这意味着数据的大小会影响性能。

优化首屏

首屏的定义有很多种,但我们接下来所讨论的首屏是指:

业务上来看第一次有意义的渲染。比如如果是列表页面,那就是列表渲染出来的时刻。

控制小程序资源包大小

当用户访问一个小程序时,支付宝客户端会首先从 cdn 上下载小程序资源包,因此资源包大小会影响小程序的启动性能。

优化建议:

  • 及时删除没有用的图片资源(所有图片资源都会默认打包进去)
  • 控制图片的大小,不要使用大图片(稍大点的图片建议走cdn)
  • 及时清理无用的代码

将数据请求提前到 onLoad

小程序运行时,会先触发页面的 onLoad 生命周期函数,然后将页面的初始数据(Pagedata)从 worker传递到 webview进行一次初始渲染,等待页面初始渲染完成之后,才会从 webview 发出通知到 worker,此时才会触发 onReady 生命周期函数。

在我们分析的一些案例中,有些小程序会在 onReady 中发出请求,这会延缓首屏的渲染。

优化建议:

  • 将数据请求提前到 onLoad

控制首屏一次性渲染的节点数量

当业务请求回来后,通常会调用 setData 触发页面的重新渲染。内部的执行过程如下:

  • 数据从 worker 传递到 webview
  • webview 上根据传过来的数据构造虚拟dom,并与之前做 diff(从根节点开始),然后渲染。
    由于 workerwebview 通信时,数据需要序列化,然后到了 webview 需要执行 evaluateJavascript,因此如果一次性传输很大的数据,会影响首屏渲染的性能。

另外,如果 webview 上构造的节点过多,层级嵌套太深(在我们分析的案例中,有的小程序列表页面会一次性渲染超过100个列表项,每个列表项又有嵌套内容,而实际上整个屏幕可能只是显示不到10个), 这样就会导致 diff 时间较长,同时由于是首屏渲染,会一次性构造很多 dom,同样会影响首屏渲染的性能。

优化建议:

  • setData 数据量不易过大,不要一次性传递一个非常长的列表
  • 首屏时不要 一次性 构造太多节点,服务端可能一次请求过来有很多条数据,不要一次性 setData,可以先 setData 一部分数据,然后等待一段时间(比如400ms,具体需要业务调节)再调用 $spliceData 将其他数据传输过去

setData

setData大概是小程序中使用最为频繁的接口了,因为页面上的任何变化都需要其去触发,但是能够触发页面去重新渲染的却不止一个 setData。如下四个接口都会触发 webview 上页面的重新渲染。

  • Page.prototype.setData: 触发整个页面做diff
  • Page.prototype.$spliceData: 针对长列表做优化,避免每次传递整个列表,触发整个页面做diff
  • Component.prototype.setData: 只会从对应组件节点开始做diff
  • Component.prototype.$spliceData: 针对长列表做优化,避免每次传递整个列表,只会从对应组件节点开始做diff
    优化建议:

  • 避免非常频繁的触发 setData 或者 $spliceData,不管是页面级别的还是组件级别的。在我们分析的案例中,有些页面有倒计时逻辑,但是有的倒计时过于频繁触发(ms级别的触发)。

  • 需要频繁触发重新渲染的时候,避免使用页面级别的 setData$spliceData, 将这一块封装成 自定义组件,然后使用组件级别的 setData$spliceData 触发组件重新渲染。
  • 长列表的数据触发渲染时,使用 $spliceData 每次追加数据,而不必每次都传递整个列表。
  • 复杂页面建议多封装成自定义组件,减少页面级别的 setData
    示例:

推荐指定路径设置数据:

  1. this.setData({
  2. 'array[0]': 1,
  3. 'obj.x':2,
  4. });

而不是

  1. const array = this.data.array.concat();
  2. array[0] = 1;
  3. const object={...this.data.obj};
  4. obj.x=2;
  5. this.setData({array,obj});

更不是(违反不可变数据原则)

  1. this.data.array[0]=1;
  2. this.data.obj.x=2;
  3. this.setData(this.data)

长列表使用 $spliceData

  1. this.$spliceData({ 'a.b': [1, 0, 5, 6] })

注意

有时候,我们将业务逻辑封装到了组件中,这样当仅仅需要组件对应的ui需要重新渲染时,我们只需要在组件内部调用 setData 触发组件重新渲染。但是,我们可能有时需要从页面上去触发组件重新渲染,比如我们在页面上监听了 onPageScroll 事件,当事件触发时,我们需要通知对应组件重新渲染,这个时候怎么做呢?

  1. // page.js
  2. Page({
  3. onPageScroll(e) {
  4. if (this.xxcomponent) {
  5. this.xxcomponent.setData({
  6. scrollTop: e.scrollTop
  7. })
  8. }
  9. }
  10. })
  1. // component.js
  2. Component({
  3. didMount(){
  4. this.$page.xxcomponent = this;
  5. }
  6. })

我们可以在组件的 didMount 中将组件挂载到对应的页面上,然后就可以在页面中调用组件级别的 setData 去仅仅触发组件的重新渲染了。

使用 key

在 for 中使用 key 来提高性能。

推荐

  1. <view a:for="{{array}}" key="{{item.id}}"></view>
  2. <block a:for="{{array}}"><view key="{{item.id}}"></view></block>

注意 key 不能设置在 block 上

原文: https://docs.alipay.com/mini/framework/performance-tips