网页的性能优化是前端开发老生常谈的热门话题,其中微信小程序因其页面双线程架构设计,所以性能优化的手段跟传统的 H5 应用不太一样。今天主要介绍一下微信小程序页面双线程架构的特性给页面渲染带来的一些影响,以及应对的一些渲染性能调优策略。为了叙述方便,下文会把微信小程序简称为小程序。
小程序的双线程架构
与传统的浏览器Web页面最大区别在于,小程序的是基于 双线程 模型的,在这种架构中,小程序的渲染层使用 WebView 作为渲染载体,而逻辑层则由独立的 JsCore 线程运行 JS 脚本,双方并不具备数据直接共享的通道,因此渲染层和逻辑层的通信要由 Native 的 JSBrigde 做中转。
小程序更新视图数据的通信流程
每当小程序视图数据需要更新时,逻辑层会调用小程序宿主环境提供的 setData 方法将数据从逻辑层传递到视图层,经过一系列渲染步骤之后完成UI视图更新。完整的通信流程如下:
小程序逻辑层调用宿主环境的 setData 方法。
逻辑层执行 JSON.stringify 将待传输数据转换成字符串并拼接到特定的JS脚本,并通过evaluateJavascript 执行脚本将数据传输到渲染层。
渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染。
WebView 线程开始执行渲染时,待更新数据会合并到视图层保留的原始 data 数据,并将新数据套用在WXML片段中得到新的虚拟节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。同时,将新的节点树替换旧节点树,用于下一次重渲染。
引发渲染性能问题的一些原因
在上述通信流程中,一些不恰当的操作可能会影响到页面渲染的性能:
setData传递大量的新数据
数据的传输会经历跨线程传输和脚本编译的过程,当数据量过大,会增加脚本编译的执行时间,占用 WebView JS 线程。
下图是我们做的一组测试统计:在相同网络环境下,各个机型分别对大小为 1KB 、 2KB 、 3KB 的数据执行 setData 操作所消耗的时间。
从图中可以看出, setData 数据传输量越大,数据传输所消耗的时间越大。
频繁的执行setData操作
频繁的执行 setData 会让 WebView JS 线程一直忙碌于脚本的编译、节点树的对比计算和页面渲染。导致的结果是:
页面渲染结果有一定的延时。
用户触发页面事件时,因 WebView JS 线程忙碌,用户事件未能及时的传输到逻辑层而导致反馈延迟。
过多的页面节点数
页面初始渲染时,渲染树的构建、计算节点几何信息以及绘制节点到屏幕的时间开销都跟页面节点数量成正相关关系,页面节点数量越多,渲染耗时越长。
每次执行 setData 更新视图, WebView JS 线程都要遍历节点树计算新旧节点数差异部分。当页面节点数量越多,计算的时间开销越大,减少节点树节点数量可以有效降低重渲染的时间开销。
渲染性能优化
基于引发渲染性能问题的原因,我们可以制定一些优化策略来避免性能问题的发生。
setData优化
setData 作为逻辑层与视图层通信的媒介,是最容易造成渲染性能瓶颈的 API 。我们在使用 setData 时应该遵循一些规则来尽可能避免性能问题的发生:
减少 setData 数据传输量
仅传输视图层使用到的数据,其他 JS 环境用到的数据存放到 data 对象外。
合理利用局部更新。 setData 是支持使用 数据路径 的方式对对象的局部字段进行更新,我们可能会遇到这样的场景: list 列表是从后台获取的数据,并展示在页面上,当 list 列表的第一项数据的 src 字段需要更新时,一般情况下我们会从后台获取新的 list 列表,执行 setData 更新整个 list 列表。
// 后台获取列表数据 const list = requestSync(); // 更新整个列表 this.setData({ list });
实际上,只有个别字段需要更新时,我们可以这么写来避免整个 list 列表更新:
// 后台获取列表数据 const list = requestSync(); // 局部更新列表 this.setData({ 'list[0].src': list[0].src });
降低 setData 执行频率
在不影响业务流程的前提下,将多个 setData 调用合并执行,减少线程间通信频次。