小程序动画性能优化实践

文章目录
  1. 1. 前言
  2. 2. 正文
    1. 2.1. 背景
    2. 2.2. 图片预加载
      1. 2.2.1. 方案
      2. 2.2.2. 方案考量
    3. 2.3. 图片显示
      1. 2.3.1. 方案
      2. 2.3.2. 方案考量
    4. 2.4. 调优
    5. 2.5. 结论

前言

本文4034字,阅读大约需要11分钟。

总括: 本文以一个实际上线的项目为案例,从初始方案的思考,到方案对比选择考量,再到最后的实践优化,向读者介绍了如何在小程序中优化一个动画,从而提升页面性能,对了解小程序基本运作原理和性能优化有一定的启发。

笑着哭了,是长大成人才懂的无奈

正文

背景


该项目的诉求是实现一个3D效果的地球,并支持转动动画效果,但小程序不支持webGL,我们有考虑使用webview的方案,但该产品涉及动态分享文案,另外webview开发工作量也比较大,最终给出的解决方案是通过在小程序中一帧一帧播放图片来模拟实现3D转动的效果。经过考量,最终敲定的图片数目为:144png格式图片。每张图片经过压缩后在30k左右,最终的效果是每125ms切换一张图片。这里有两个难点需要去考虑:一个是对于144张图片的预加载(图片预加载方案),一个是对于这144张图片的处理方式(图片显示方案)。

图片预加载


方案

何为图片预加载?所谓预加载就是提前加载,把图片缓存到本地,这样在应用中用到该图片的时候就可以直接从缓存里取,对应响应速度就会变快了,就好像我们在吃自助餐的时候,会提前把一些食物拿到桌子上一样,这样我们吃起来就方便了许多。我们知道在传统web中我们往往可以通过Image对象来对图片进行预加载,但小程序没有是Image对象的,因此就需要变通一下。小程序虽然没有提供Image对象,但却有wx.downloadFile可以使用,当然换个思路我们也可以通过在页面中隐藏很多个image组件,通过监听image组件的加载情况来进行预加载。下面是这两种方案的示例代码:

1. 页面隐藏image,监听onload事件来缓存图片,示例代码如下:

1
2
3
4
5
6
7
8
9
<image
hidden
wx:for="{{preLoadImageList}}"
wx:key="{{index}}"
data-id="{{item}}"
src="{{item}}"
bindload="imageOnload"
binderror="imageError"
/>

2. 通过微信提供的wx.downloadFile API提前下载图片进行缓存。

1
2
3
4
5
6
7
8
wx.downloadFile({
// 仅为示例,并非真实的资源
url: 'https://example.com/img.png',
success(res) {
// 图片临时路径
console.log(res.tempFilePath)
}
})

上面列出了两个问题的解决方案,首先我们先来看下实际操作中图片预加载方案的是与非:

方案考量

图片预加载的两种方案(image组件和wx.downloadFile)实际上没有本质上的区别,但在某些场景使用wx.downloadFile进行预加载可以更为方便的处理我们的业务逻辑。
另外在实际处理过程中笔者发现在iOS系统中(安卓系统中正常)通过隐藏image组件预加载图片的方式,更为的不可控。所谓的不可控是指,当我们的image组件的hidden属性设置为true的时候图片会被不定时的回收一部分。另外笔者测试发现把组件的display属性设置为none的时候也是一样的结果。这也进一步说明了,小程序对于组件hidden属性的处理确实是切换组件的display属性,即元素依然存在于DOM树中。关于图片会被回收这块,官网其实也有说明:

iOS 上,小程序的页面是由多个 WKWebView 组成的,在系统内存紧张时,会回收掉一部分WKWebView。从过去我们分析的案例来看,大图片和长列表图片的使用会引起WKWebView 的回收。

(备注:WKWebView是iOS系统中渲染层的运行环境)

但从官网的表述来看,小程序是回收整个页面,但实际并不是这样,所以猜测WKWebView对于这块处理方式应该是当图片imagedisplay属性为none的时候不对这些图片进行缓存或者缓存时间很短。

那么wx.downloadFileAPI 和display没关系了是不是就可以缓存住了呢,也不绝对,实际操作过程中发现和第一种方案相比并没有什么不同,在iOS中部分图片还是会不定时的被回收掉一部分导致我们的应用会有掉帧的现象,这种掉帧是因为图片需要重新加载一遍,而在这个过程中图片组件经历了空白到图片加载完成的过程,所以会有很明显的掉帧现象。微信对于这块的处理看起来和WKWebView有些相似,都遵循了一个原则,即当图片不会显示在用户界面那么不进行缓存或者缓存时间很短。

经过以上分析图片掉帧的原因基本确定不在图片预加载的方式上面了,大概率就是和图片的显示方案有关系,接下来我们再看下图片显示方案:

图片显示


方案

图片显示方案主要分为两类,一类是使用canvas进行绘图,一类是去修改DOM,修改DOM又有两种方案,一个是通过单元素image组件的图片链接来切换图片,一类是展示144个元素,控制144个元素的显隐来切换图片。下面我们来一一看下这三种方案:

1. 采用canvas绘图的方式

该方案实际操作过程中发现会有掉帧的现象,主要是绘制的前提是先要把图片下载下来,然后再进行绘制,即使我们先将图片下载下来,直接使用本地路径去绘制也会有明显的闪动的问题。
另外使用canvas绘图限制也会比较多,canvas组件在小程序内部属于原生组件。所谓的原生组件是指由客户端创建的,脱离WebView渲染流程外的组件。 原生组件有很多的限制,比如:

  1. 层级是最高的,无法通过设置z-index来让普通组件覆盖在原生组件之上;
  2. 无法对原生组件设置 CSS 动画;
  3. 无法定义原生组件为position: fixed
  4. 不能在父级节点使用 overflow: hidden 来裁剪原生组件的显示区域;

其中第一条是常能遇到的限制,设想你的主页面中有一个canvas绘制的元素,但一些业务逻辑又需要有弹窗,这时候就很棘手,canvas会覆盖在弹窗之上,无论你怎么设置弹窗的z-index都没效果。
当然,小程序团队也不是不通情理的。他们提供了一个名为cover-viewcover-image的组件来覆盖原生组件。但cover-view又有一个很大的限制:只支持嵌套cover-viewcover-image。(更多的cover-view限制可以去官网查看,这里不再过多介绍)。因此canvas不适合本项目需求。

2. 只展示当前image组件,通过改变组件的图片链接来更换图片。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<image
catchtouchstart="touchStart"
catchtouchmove="touchMove"
catchtouchend="touchEnd"
src="{{imageUrl}}"
/>
<!-- 或 -->
<view
catchtouchstart="touchStart"
catchtouchmove="touchMove"
catchtouchend="touchEnd"
style="background-image:url({{imageUrl}})"
/>

3. 罗列所有的元素,通过控制所有图片显隐达到切换图片的目的。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<view
catchtouchstart="touchStart"
catchtouchmove="touchMove"
catchtouchend="touchEnd"
>
<image
wx:for="{{imageList}}"
wx:for-index="idx"
wx:key="{{idx}}"
data-id="{{item}}"
bindload="imageOnload"
binderror="imageError"
src="{{item}}"
style="transform: {{idx === index ? 'translate(0, 0)' : 'translate(-100%, 0)'}};"
/>
</view>

方案考量

1. 只展示当前image组件,通过改变组件的图片链接来更换图片。

由前面预加载方案的结论我们可以发现方案一不可行。

结论: 问题的根源在于小程序对图片的回收和微信的缓存机制,导致更换图片链接的时候会不可控的出现掉帧闪屏的现象。

2. 罗列所有的元素,通过控制图片显隐达到切换图片的目的。

这样看来我们只有方案二可以使用了,但方案二要多渲染143个节点,在小程序中,setData的性能是和页面的节点数量成正比的,节点越多理论上setData一次的效率就越低。但没有更完美的方案….只能硬着头皮上了。代码改成上述方案二之后,iOS系统很流畅不会掉帧闪屏了,但安卓基本都卡到没法看,页面滑动也成问题,滑动一下,几秒后才会有响应,这个其实和预期也相符,setData本身携带的数据比较大(页面还有其它的逻辑数据),节点数量也增加了数倍,对性能的影响是指数爆炸式的增长。官方也有解释:

  • Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;
  • 渲染有出现延时,由于 WebViewJS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;

官网解释十分冗长实际我感觉不利于新手理解。这个其实要从小程序的运行环境说起。小程序的运行环境分成渲染层和逻辑层,其中WXML 模板和WXSS样式工作在渲染层,JS脚本工作在逻辑层,他们之间的通信如下图所示(图片来自官网):
小程序的通信模型
如上,我们每次在逻辑层(JS代码)进行setData的时候,首先是由逻辑层传递到Native,再由Native传给渲染层(视图层)进行处理。好了这个时候,你知道了渲染层和逻辑层是啥之后再去看上面官网的解释就很顺畅了。

在传统的web应用中,我们可能是直接去修改DOM来更改某个元素,一条道路走到头。而小程序不是这样,没法一条路走到头,Native扮演着”桥”的角色来进行转发。这是我们在安卓上会感觉到卡顿的根本原因,因为会走一段”弯路”。

因此我们需要提高安卓下面的页面性能,而开启硬件加速成了一条捷径。我们把上述代码改成了如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<view
catchtouchstart="touchStart"
catchtouchmove="touchMove"
catchtouchend="touchEnd"
>
<image
wx:for="{{imageList}}"
wx:for-index="idx"
wx:key="{{idx}}"
data-id="{{item}}"
bindload="imageOnload"
binderror="imageError"
src="{{item}}"
style="transform: {{idx === index ? 'translate3d(0, 0, 0)' : 'translate3d(-100%, 0, 0)'}};"<!-- 修改点 -->
/>
</view>

开启硬件加速后安卓确实不卡顿了,页面正常转动也可以左右滑动。但…苹果iOS重返老路疯狂闪屏…其实代码只在translate3dtranslate的区别,查找相关问题: 关于translate3D动画在iphone上闪屏的问题,简单的说导致这个问题的根源在于WebKit会把应用了translate3d的元素单独划分为一个层进行渲染,但对其内部子元素并不会,解决方案就是将子元素也缓存起来,作为单独的层进行渲染。但小程序image组件实际是多层的DOM结构,臆想结构如下:

1
2
3
4
5
<div style="display:inline-block; ...">
<div style="display:inline-block;background-image:url({{url}});...">
...
</div>
</div>

在小程序中是没法控制这个image组件的子元素的,暂时的解决方案通过区分当前用户的操作系统来决定是否启动GPU加速,即是否使用translate3d

调优


自定义组件

从基础库版本 1.6.3 开始,小程序开始支持组件化编程,对于动画组件也有得天独厚的一面,官网是这样说的:

  • 使用连续使用setData 来改变界面的方法也可以达到动画的效果。这样可以任意地改变界面,但通常会产生较大的延迟或卡顿,甚至导致小程序僵死。此时可以通过将页面的 setData 改为 自定义组件 中的 setData 来提升性能。

  • 出于性能考虑,使用 usingComponents 时,setData内容不会被直接深复制,即 this.setData({ field: obj })this.data.field === obj 。(深复制会在这个值被组件间传递时发生。)

小程序的自定义组件是有一个单独的构造函数Component来进行实例化的,因此它的setData和主页面的setData互不影响。应用到这个业务场景的确再适合不过了,将这个图片更改和滑动逻辑单独封装为一个组件,再由主页面进行引入,不影响主页面性能的同时也能提供这部分数据更改的响应速度。

WXS响应事件

WXS是小程序自己的一套脚本语言,语法和JS基本一致,可以直接嵌套在WXML里进行使用。我们前面说过JSWXML通信是需要Native这个”桥”的,没法直接进行DOM操作。而WXS的存在就可以直接去操作DOM

其实在传统的Web开发中,通过频繁图片来做一个交互式动画是比较常见的,很多情况下直接去操作DOM即可。小程序不一样,小程序是没有DOM API的,现在网上很多的说法是小程序没有DOM,其实不准确,请记住:小程序有DOM但没有DOM APIBOM API。因此它不能直接用jQuery这种库。

WXS使用动画中的应用原理也是基于此,我们不再进行setData了,那是JSWXML交互用的API,在WXS里面我们可以直接去修改DOM树,但WXS响应事件从小程序基础库 2.4.4 才开始支持,考虑到兼容性,因此本例中不采用该方案。对于WXS去修改DOM的更多细节请看官网WXS响应事件,这里不再赘述。

结论


综上,通过预加载和控制每个图片显隐我们解决了图片闪烁的问题,通过自定义组件和开启GPU加速我们将动画卡顿的问题解决。最终对该动画的处理方案是采用在自定义组件中进行,使用wx.downloadFile进行预加载图片,罗列所有的元素,通过控制每个图片显隐达到切换图片的目的,另外通过判断安卓和iOS系统来判定是否开启GPU加速。这是该动画最终的技术方案。在此过程中我们罗列了很多其它的方案,也可以作为参考,在某些业务场景下也许是更好的解决方案。以上方案更多的侧重于依赖小程序所拥有的一些能力,抛开小程序本身,实际对于图片的考量并不是很多,比如图片的格式(使用webpapng格式的图片)、大小(是否还有可压缩的空间)等。


能力有限,水平一般,欢迎勘误,不胜感激。

转载请获本人授权,并注明作者和出处。

订阅更多文章可关注公众号「前端进阶学习」,回复「666」,获取一揽子前端技术书籍

前端进阶学习

如果您觉得我的文章对您有用,请随意打赏。

您的支持将鼓励我继续创作!

人过留名,雁过留声
听听你的声音

回复 Username 留言: content x