| 导语 小程序的部分组件是由客户端渲染的原生组件,本文使用的 video 组件属于其中之一。视频列表涉及多个 video 组件的渲染、资源加载、滑动,处理不当会带来比较大的性能消耗。本文通过多种方案的对比,探讨视频列表渲染的最佳姿势,达到性能优化的目的。
一、背景
qq 小程序应用商店上的“值得一玩”模块,是由多个横向排列的视频组成的视频列表。该模块始终有一个视频完全处于可视区域,下一个视频的一部分露出。左右滑动列表切换下一个视频到可视区域,在 wifi 条件下自动播放可视区域视频。效果如下图所示:
根据业务需要,视频列表采用腾讯视频插件 txv-video 代替 video 组件,txv-video 底层也是 video 组件,只是在上面多封装了一层。
二.渲染方案对比
方案1:一次性渲染所有的 video 组件
刚拿到这个需求,想着 video 组件数量不多(最多5个),直接全部渲染影响应该不大。效果如下所示:
可以看到,模块加载时间过长,出现了 1-2s 的白屏现象。
下面从原生组件的渲染过程来解释原因。原生组件有非同层渲染、同层渲染2种渲染方式。
非同层渲染下,video 组件的渲染过程:
1. WebView 渲染一个占位元素,包括创建组件,计算组件位置、大小,通知客户端。
2. 客户端在相同的位置上,根据宽高插入一块原生区域进行渲染。
同层渲染下,video 组件的渲染过程(ios和安卓渲染方式不同,此处以安卓为例):
1. WebView 创建一个 embed DOM 节点并指定组件类型。
2. chromium 内核会创建一个 WebPlugin 实例,并生成一个渲染层 RenderLayer。
3. WebView 通知客户端创建原生组件。
4. 客户端将原生组件的画面绘制到步骤2创建的 RenderLayer 所绑定的 SurfaceTexture 上。通知 chromium 内核渲染该 RenderLayer 。
5. chromium 渲染该 embed 节点并上屏。
在非同层渲染下,原生组件的层级永远高于 Webview 的层级(无论 z-index 设置为多少),当组件位置发生改变时, Webview 通知客户端更新。这样会导致在切换视频时,video 组件位置的更新速度跟不上滑动速度,出现“连在一起”的现象。
安卓的同层渲染真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点。当组件的位置发生改变时,WebView 更新,不用与客户端通信。目前 qq 小程序的 video 组件已经支持同层渲染。
可以看到,渲染过程涉及 WebView、客户端、内核的一系列操作。因此,当 video 组件个数越多时,渲染这些 video 组件耗费的时间越长。
方案2:开始只加载1个 video 组件,移动到目标区域后再加载 video 组件
根据方案1的结论,减少模块加载时渲染的video 组件个数。开始只渲染1个 video 组件,其余不在可视区域的用 image 标签代替,移动到可视区域后再渲染 video 组件。效果如下所示:
可以看到,相比方案1,模块加载时间明显减少。
同时发现:在 wifi 条件下自动播放可视区域视频,左右滑动时会发生卡顿现象。如下所示:
尝试了开启 3d 加速、先暂停视频再滑动(避免直接滑动视频带来的性能问题)等方法都没有明显的改进。在非 wifi 情况下,不自动播放可视区域视频,不会发生卡顿现象。
滑动切换播放视频的过程如下图所示:
视图层 Webview 处理 touch 事件,调用 callMethod 与 逻辑层 Appservice 通信;Appservice 收到当前 video 组件的 index 信息后,setVid 加载资源,执行 play 操作, 通知客户端;客户端播放当前视频,暂停上一个视频。
从表象上看,卡顿现象的发生与滑动到目标区域后是否播放视频有关。是 Appservice 与客户端的通信阻塞了 Webview 的操作?还是播放视频导致了卡顿的发生呢?
小程序的卡顿通常发生在逻辑层与视图层频繁地通信、页面节点数过多等情况下,Appservice 与客户端的简单一次通信并不会造成卡顿的发生,猜想是播放视频导致了卡顿。去除自动播放视频的操作,手动控制 video 组件播放或暂停,切换视频时发现卡顿依然明显。卡顿与滑动到目标区域后是否立即播放视频没有关系,而与播放过的video组件个数有关,播放过的video组件个数越多,切换时越卡顿。
下面从 chromium 内核对 <video> 标签的处理来解释原因。
chromium 是通过 WebKit 解析网页内容的。当 WebKit 遇到 <video> 标签时,就会创建一个播放器实例。WebKit 并没有自己实现播放器,而仅仅是创建一个播放器接口。通过这个播放器接口,可以使用平台提供的播放器来播放视频的内容。当创建 <video> 标签时,仅仅创建了类型为 HTMLMediaElement 的 DOM 节点。当为 video 组件的 src 赋值时,会调用接口创建播放器,进行视频资源信息加载、视频解码等一系列操作,“真正”渲染 video 组件。
上述操作会占用一部分系统资源,播放过的 video 组件个数越多,占用的系统资源越多,切换视频时越卡顿。即使暂停视频也没用,video组件实例仍然存在没被销毁,依旧占用系统资源。
方案3:video 组件实例复用
根据方案1、2的结论,减少 video 组件实例个数。考虑采用3个 video 组件,索引为 index % 3 的 video 组件用来播放当前视频,索引为 ( index 1 ) % 3 的 video 组件用来预加载下一个视频,索引为( index 2 ) % 3 的 video 组件用来缓存上一个视频。在左右滑动切换时仅更改这3个 video 组件的 transform,达到视觉隐藏和实例复用的目的。从需求背景可以看到,本需求要求下一个视频的一部分露出,与本方案不太符合,本方案更适合一个视频占满整个可视区域的使用场景,比如微视无限列表。
方案4:video 组件即用即毁,其余用 image 标签代替
从上面的分析可以得到,减少模块加载时间和提高视频切换性能的关键在于减少 video 组件实例数,需要及时销毁当前没有使用的 video 组件实例。因此,只渲染可视区域的 video 组件,其余用 image 标签代替,当 video 组件滑出可视区域后,及时销毁该 video 组件实例。
关键代码如下所示:
<txv-video
代码语言:javascript复制 class="topic-video"
wx:if="{{ videoVid[index] && viewportList[index] }}"
vid="{{ videoVid[index] }}"
playerid="topic-video-{{ index }}"
poster="{{ item.videoCover }}"
bindplay="playVideo"
bindpause="pauseVideo"
muted="{{ true }}"
showMuteBtn="{{ true }}"
enableProgressGesture="{{ false }}"
data-app="{{ item }}"
bindtap="tapVideo"
autoplay="{{ videoVid[index] }}"
showFullscreenBtn="{{ false }}"
showCenterPlayBtn="{{ false }}"
></txv-video>
<view wx:if="{{ !videoVid[index] || !viewportList[index] }}"
代码语言:javascript复制class="topic-video-image">
<image src="{{ item.videoCover }}" class="topic-video-cover">
代码语言:javascript复制</image>
<image
src="../../images/ad-friend-watch/topic-video-play-btn.png"
class="topic-video-play-btn"
bind:tap="tapVideo"
></image>
</view>
视频切换效果如下所示:
可以看到,切换视频时不存在卡顿现象,性能得到了明显的提升。
本方案对 video 组件即用即毁,滑动到可视区域时才渲染组件,相比 video 组件实例复用,花费的时间会不会多很多呢?
从方案2中的分析可以得到,在 video 组件的 src 赋值前,仅创建了一个 DOM 节点,该步骤的时间花销较小。在 video 组件的 src 赋值后,才“真正”渲染 video 组件。所以,对于切换到 src 没有被赋值的 video 组件,本方案和 video 组件实例复用的时间花销差不多。但是,对于视频被播放过再切回该视频的情况,因为该 video 组件已经被销毁,会再次经历渲染 video 组件、加载资源等操作,有一定的时间损耗和用户流量的损耗。考虑到非wifi情况下不会自动播放视频,视频时长较短(15-30秒),且用户来回滑的行为概率较小,相比明显卡顿甚至卡死现象,还是更能让人接受。
数据分析方法入门
从0开始打造UI框架:动态化框架Scrollview物理学算法解析
直播插件体系设计
喜欢本文?快点“在看”支持一下↓↓