对于setData优化相关,官方已做了详细的说明和优化建议,那为什么你还需要看这篇文章?
因为本篇可以解决你对setData
的所有疑问:
- 彻底搞清楚为什么要优化 setData ?小程序为什么要设计 setData 来更新数据?
- setData 都做了哪些事?为什么 setData 那么消耗性能 ?优化完能带来多少性能提升?
- 有哪些优化方法和思路?
- 人工优化太麻烦?别怕,有问题就有对策!
为什么要重视优化setData?
1. 由于小程序双线程模型的运行机制,setData会产生额外的通信消耗
-
网页开发中JS主线程(负责解析和执行js代码,例如V8引擎就在JS线程上运行)和渲染线程(负责DOM计算和绘制相关)是互斥的(因为渲染线程和JS线程都可以操作
DOM
,同时运行容易发生混乱),而小程序的逻辑层和渲染层是分开的。 -
小程序采用
AppService
和WebView
的双线程模型(App 进程和 WebView 进程中的两个线程),视图渲染采用基于WebView
和原生控件混合渲染的方式,这两个线程没有互斥的关系(因为处于不同的进程),可以在Native
客户端的协调下有条不紊的同时执行。官方文档中有这样一段话,解释了为什么采用双线程模型:当小程序基于 WebView 环境下时,WebView 的 JS 逻辑、DOM 树创建、CSS 解析、样式计算、Layout、Paint (Composite) 都发生在
同一线程
,在 WebView 上执行过多的 JS 逻辑可能阻塞渲染,导致界面卡顿。以此为前提,小程序同时考虑了性能与安全,采用了目前称为「双线程模型」的架构。上面说的 “同一线程” 其实指的是
webView
的渲染进程(渲染进程中包含渲染线程和JS主线程等),网页开发中提到的渲染线程与JS主线程互斥和阻塞的问题在WebView
环境下也不例外,所以小程序则将webView
中的 JS 逻辑部分拆到App级(App进程中不同于WebView环境,无法访问window
document
等对象)的JS线程中运行即所谓的AppService
,WebView
与App
是独立的运行环境,虽然可以同时运行不会发生阻塞,但是也无法共享数据,所以要依靠Native
(微信客户端提供的WeixinJsBridge) 来通讯,那么必然会产生通信耗时。
2. setData整个流程执行任务较多,耗时较大
调用 setData
方法时,会产生如下操作:
-
逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer(监听器) 等;
-
将 data 从逻辑层传输到视图层;
这个过程需要进行数据的序列化(
JSON.stringfy()
)、跨线程/进程的数据传输(两个线程需要Native
来中转,即上面说的通讯)、数据的反序列化(JSON.parse()
)。JSON.stringfy()
内部会进行递归、遍历、多个判断、字符串拼接,键越多或嵌套越深耗时则越多。iOS/iPadOS/MacOS 上,数据传输是通过
evaluateJavascript
实现的,还会有额外 JS 脚本解析和执行的耗时。 -
视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新,完成后触发
setData
回调。
双线程模型的优缺点
优点:
-
将逻辑层和渲染层隔离开,用户无法直接操作
DOM
,提供了相对封闭和安全的运行环境。 -
JS执行不会阻塞或干扰
webView
渲染,但是大部分情况下视觉都要依赖JS中处理的数据,JS如果被阻塞(阻塞原因有逻辑重或请求慢等)了就不会通知视图去更新(即执行setData
),所以这条优点其实意义不是很大!所以小程序官方搞出了 初始渲染缓存,会缓存初始data(不包含setData)的渲染结果,详情请看官方文档。
-
所有的页面和组件的逻辑(js)都在一个线程(
AppService
)里,使用同一个上下文环境,比较好做状态共享或跨页面通讯。可以使用global对象试一下,如 global.a = 1; 另一个页面输出一下gobal可以看到a为1;
缺点:
-
缺点在于每一次数据传递都要进行一次线程之间的通信,业务逻辑跟渲染层天然隔离,造成通信开销大、延迟高等问题,通信越频繁、数据量越大,则性能瓶颈越严重。
-
每个页面都创建一个
WebView
线程处理,有更多的内存、时间开销。 -
渲染层和逻辑层状态要维护两份,进一步加重内存、时间开销,并且没有办法完全保证两份数据状态实时保持一致,例如仅使用
this.data
更新数据而不是通过setData
时,那么实际渲染的值与逻辑层的值就不一致,某些场景下会造成非预期的问题。尤其当 this.data.obj 这个obj指向一个引用地址的时候,带来的问题更加不可预期。
this.viewModal = {obj: {a: 3}}; this.setData({obj: this.viewModal.obj}); // 此时逻辑层的data.obj已经指向viewModal.obj对象的地址 this.viewModal.obj.a = 666; this.data.obj.a === 666; // true // 之后无论你在任何地方不小心更改了this.viewModal.obj,那么this.data.obj也变了,而此时在渲染层的数据还是旧数据,你本意是想通过this.data获取当前被渲染的值,但是这时候可能值已经并不符合你的预期了。
总体上从开发者的角度来看,小程序的双线程模型架构并不是一个很好的架构,逻辑层与渲染层隔离,带来的问题远远比它解决的问题更多。
除了状态共享和跨页面通信外,几乎对开发者来说没啥吸引力,但是依旧很鸡肋:
- 一个应用里面的多个页面,你认为是共享的状态多,还是独立的状态多?
- 用户在操作页面时,更关心跨页面通信效率,还是更关心当前页面的渲染效率(页面里的内容是否顺滑)?
我们当然更关心性能,但是从微信的角度来说,他们想既能享受 web
生态的好处的同时也能限制 web
的开放性,增强自己对平台内容的管控程度,从禁用 eval
和 new Function()
上就能看出一二。
当然微信也知道架构所带来的性能问题,所以发明了 WXS
,让一部分 js
代码能在渲染层跑,部分解决通信消耗和延迟的问题,只能满足很小一部分场景,依旧很鸡肋。
理论上是可以在wxs中访问到window对象的,因为wxs代码运行在
webView
中,但是微信对wxs功能做了阉割限制,只提供很少一部分功能,尤其不能让用户操作DOM。
综上所述,优化setData
是一个小程序应用无法忽略的关键一步,它占了小程序性能体验的大头,理应贯穿整个小程序开发周期。
优化建议
官方提供了5点优化建议,copy过来方便分析:
1. data 应只包括渲染相关的数据
setData 应只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。
- ✅ 页面或组件的 data 字段,应用来存放和页面或组件渲染相关的数据(即直接在 wxml 中出现的字段);
- ✅ 页面或组件渲染间接相关的数据可以设置为「纯数据字段」,可以使用 setData 设置并使用 observers 监听变化;
- ✅ 页面或组件渲染无关的数据,应挂在非 data 的字段下,如
this.userData = {userId: 'xxx'}
; - ❌ 避免在 data 中包含渲染无关的业务数据;
- ❌ 避免使用 data 在页面或组件方法间进行数据共享;
- ❌ 避免滥用 纯数据字段 来保存可以使用非 data 字段保存的数据。
官方为了优化减少双线程模型带来的性能问题可谓是操碎了心,为了不让开发者写太多与渲染层无关的数据给通信增加负担,又搞出了“纯数据字段”,用来在通信时过滤无用数据,虽然说是极其不优雅但也是无奈之举。
看一下这条:避免使用 data 在页面或组件方法间进行数据共享; 意思就是不要通过data.xxx的方式来共享数据,例如,组件获取页面实例然后通过PageInstance.data.xxx来获取页面的数据,而这个xxx与渲染层却无任何关系。
省流: 避免在
data
中添加与渲染层无关的数据,无论用来干什么。
2. 控制 setData 的频率
每次 setData 都会触发逻辑层虚拟 DOM 树的遍历和更新,也可能会导致触发一次完整的页面渲染流程。过于频繁(毫秒级)的调用 setData
,会导致以下后果:
- 逻辑层 JS 线程持续繁忙,无法正常响应用户操作的事件,也无法正常完成页面切换;
- 视图层 JS 线程持续处于忙碌状态,逻辑层 -> 视图层通信耗时上升,视图层收到消息的延时较高,渲染出现明显延迟;
- 视图层无法及时响应用户操作,用户滑动页面时感到明显卡顿,操作反馈延迟,用户操作事件无法及时传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层。
因此,开发者在调用 setData 时要注意:
- ✅ 仅在需要进行页面内容更新时调用 setData;
- ✅ 对连续的 setData 调用尽可能的进行合并;
- ❌ 避免不必要的 setData;
- ❌ 避免以过高的频率持续调用 setData,例如毫秒级的倒计时;
- ❌ 避免在 onPageScroll 回调中每次都调用 setData。
省流:尽可能地减少调用
setData
。
3. 选择合适的 setData 范围
组件的 setData 只会引起当前组件和子组件的更新,可以降低虚拟 DOM 更新时的计算开销。
- ✅ 对于需要频繁更新的页面元素(例如:秒杀倒计时),可以封装为独立的组件,在组件内进行 setData 操作。必要时可以使用 CSS contain 属性限制计算布局、样式和绘制等的范围。
组件的
setData
只会触发组件内的虚拟DOM的计算,相比于整个页面会少一些通信耗时,否则会频繁触发整个页面的虚拟DOM计算。 省流:将频繁更新的页面元素封装为组件
4. setData 应只传发生变化的数据
setData 的数据量会影响数据拷贝和数据通讯的耗时,增加页面更新的开销,造成页面更新延迟。
- ✅ setData 应只传入发生变化的字段;
- ✅ 建议以数据路径形式改变数组中的某一项或对象的某个属性,如
this.setData({'array[2].message': 'newVal', 'a.b.c.d': 'newVal'})
,而不是每次都更新整个对象或数组; - ❌ 不要在 setData 中偷懒一次性传所有data:
this.setData(this.data)
。
通信过程中会有遍历操作,数据越多耗时越多,已经被渲染过的数据重复渲染毫无意义,却额外增加了很大性能消耗,例如分页列表加载场景,可以使用二维数组优化,避免下面这种写法。
const list = [...]; // length: 10
const newList = [].concat(list); // newList 会越来越大,足够大时页面会有明显卡顿,甚至崩溃,其实每次仅需更新10条,其余合并的都是已经渲染过的
this.setData({list: newList})
省流:setData 应只传入数据发生变化的字段。
5. 控制后台态页面的 setData
由于小程序逻辑层是单线程运行的,后台态页面去 setData
也会抢占前台页面的运行资源,且后台态页面的的渲染用户是无法感知的,会产生浪费。在某些平台上,小程序渲染层各 WebView 也是共享同一个线程,后台页面的渲染和逻辑执行也会导致前台页面的卡顿。
- ✅ 页面切后台后的更新操作,应尽量避免,或延迟到页面
onShow
后延迟进行; - ❌ 避免在切后台后仍进行高频的 setData,例如倒计时更新。
逻辑层上面已经说过,运行在App进程的JS线程中,JS是单线程语言,遵循事件循环机制,任务在调用栈中后进先出依次执行,上一个任务没执行完,下一个就一直等待,所以这些后台执行的
setData
也会抢占前台资源。
可视窗口外的 setData 严格来说也算“后台”运行的 setData
省流:避免在用户无法感知的场景(后台)调用setData。
最后再总结一下:
- 避免在
data
中添加与渲染层无关的数据。 - 尽可能地减少调用
setData
。 - 将频繁更新的页面元素封装为组件,减小 setData 触发的计算的范围。
setData
应只传入数据发生变化的字段。- 避免在用户无法感知的场景(后台)调用
setData
。
总之,这几点优化建议的根本要义就是调用少,数据精,即尽可能减少调用次数,尽可能让每次调用都只传输必要的数据。
优化实践
纸上得来终觉浅,理论还得去实践~
1. 删掉渲染层用不到的数据,全都改到 this
或下其他方式 --- 难度系数 ⭐️
**2. 将频繁更新的页面元素封装为组件,大多数场景我们都是在组件化开发,这点几乎没有难度,只不过需要额外留意“频繁更新”这个关键词,看有没有漏掉的 ** --- 难度系数 ⭐️
3. 检查后台运行的 setData,包括不在可视窗口内的,改成进入后台后暂停 setData,比如轮播,倒计时等场景可能为高发地段 --- 难度系数 ⭐️⭐️
鉴于人工检查、分析较为耗费精力,加一颗星
4. 减少调用 setData ,合并 setData --- 难度系数 ⭐️⭐️⭐️⭐️⭐️
看着不难做到,为啥五颗星?
由于函数式编程和函数单一职责原则,为了更好的可读性和可维护性,我们的代码往往要实现低耦合,这意味着某些场景我们不得不把 setData 分散到各个函数,而不能把它们糅杂到一起,造成的问题显而易见,每个 setData 都会产生通信消耗,那将浪费不少性能,能够完美的在性能和可维护性之间做好平衡是不容易的,大多数情况我们都是取可维护性而舍性能。
5. setData 只传入数据发生变化的字段,使用数据路径形式替换直接更新某个对象或数组 --- 难度系数 ⭐️⭐️⭐️⭐️⭐️
分页列表使用二维数组实现;
避免使用 this.setData(...obj)
,...
本身就是遍历迭代器的操作,比 forEach
性能还要差一些,如果仅仅是 obj
里的属性变化,使用数据路径形式替代,只更新必要字段,而且...
也不直观;
除此之外,还有很多不易发现或者不易判断是否发生变化的属性,在开发过程中不可避免地会被遗漏掉,全都考虑的面面俱到的话整个开发过程会极为复杂,在setData
一个属性时需要留意该属性目前可能是处于一个什么样的状态,做出判断,甚至得为了只更新变化的数据而多写很多逻辑,这样虽然可能性能有所提升,但是对于开发者来说极不友好,写个 setData
都得思前顾后。。。
还有些是我们为了更好的可读性、代码的简洁性主动忽略掉这点的。
因此,这样被重复渲染的数据在大多数项目中都绝不在少数,只是由于无法量化,且在当今的一些高性能手机上表现也过得去,所以大家也就没太在意,但是优化的空间肯定是存在的,且非常大,只不过优化成本较高,费心费力,所以此题也给 5 颗星。
至此,可优化空间这么吸引人,那绝不能带着遗憾戛然而止,有痛点就要解决掉!下一篇我们讲一下如何利用工具自动优化 setData
,让你放心地使用 setData
,开发中只需关注业务逻辑,提高开发效率和开发体验的同时也能大幅提高性能。
感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,还请三连支持一下,点赞、关注、收藏,作者会持续与大家分享更多干货。