直播这两年太火了,做直播的公司也越来越多,尤其是今年小程序针对直播组件的优化(性能优化 + 同层渲染),催生了很多的小程序直播应用。
京喜直播的小程序端上线也有几个月了,基于内部分享,当前稍加总结一下,我这里就主要讲讲直播的几个重点场景。
微信扫码,欢迎体验:
本文目录:
-
直播间消息处理
-
直播间上下滑
-
组件间通信
-
Canvas 多合一
直播间消息处理
直播相对于传统媒介,主要特点是互动性更强,因此产生了大量的互动行为,比如点赞,下单,领券等各种消息需要广播,于是就产生了大量的消息内容,那么对于这些消息我们如何处理呢?
第一种方法
直接接受到消息就渲染到页面上去,这种最简单,代码如下:
function receiveMessage(msg){
this.data.msgList.push(msg);
this.setData({
msgList:this.data.msgList
});
}
这种方式固然简单,但是在消息越来越多的时候,数组越来越大,节点会越来越多,直播间最终会卡爆,对于本来就是希望延长用户观看时长的直播,这样子显然不可取。
第二种方法
既然随着时间推移,消息会越来越多,导致节点越来越多,从而可能页面响应时间过慢,甚至卡死,那么我们就应当减少节点。处理如下:
function receiveMessage(msg){
if(this.data.msgList.length>30){
this.data.msgList.shift();
}
this.data.msgList.push(msg);
this.setData({
msgList:this.data.msgList
});
}
如上,我们页面当中的消息节点就会被控制在 30 个以内,当然还有一个很重要的,需要采用 “就地更新”的策略。
就地更新策略:内容变化的时候,无需重新创建 DOM 节点,而是共用已有的 DOM 节点,然后更新里面的内容就行了,性能大大的提升了。
消息节点不会随着时间推移而变大,满足大部分场景是没有问题的。
第三种方法
如上第二种方法,在超过 30 个节点之后,每次增加一条,再删除一条,并且采用就地更新的策略,看起来也没啥问题了。
然而在有些非常火爆的情况下,我们仍然发现了页面比较卡的问题。究其原因是 shift 性能不怎么滴,当消息量过快,执行 shift 的次数就会很频繁,也就是 msgList 修改很频繁,带来的结果就是页面一直在执行 shift 和 push ,渲染到实体 Dom 的次数很频繁,性能就跟随着大幅度下降了。
那针对这种频繁的消息,我们又该如何处理呢?
于是想着减少执行操作或者换性能更强的实现方式来实现。于是想到了 slice,splice,我们不妨来看看这三个的性能如何。
slice & splice & shift 性能对比
首先,我们使用 benchmark 来测试,测试代码如下:
var Benchmark = require('benchmark');
var suite = new Benchmark.Suite();
const MAX_COUNT = 100000;
suite
.add("shift#test", function () {
var list = [****];// 预设 30 个节点
let i = 1;
while (i < MAX_COUNT) {
list.shift();
list.push({ a: i, b: "b" });
i++;
}
})
.add("slice#test", function () {
var list = [****];// 预设 30 个节点
let i = 1;
while (i < MAX_COUNT) {
list = list.slice(1);
list.push({ a: i, b: "b" });
i++;
}
})
.add("splice#test", function () {
var list = [****];// 预设 30 个节点
let i = 1;
while (i < MAX_COUNT) {
list.splice(0, 1);
list.push({ a: i, b: "b" });
i++;
}
})
……
.on('cycle', function(event) {
console.log(String(event.target));
})
.run({ async: true });
测试数据如下:
-
shift#test x 207 ops/sec ±6.88% (78 runs sampled)
-
slice#test x 114 ops/sec ±1.89% (71 runs sampled)
-
splice#test x 80.90 ops/sec ±0.71% (67 runs sampled)
207 ops/sec :每秒钟执行测试代码 207 次。
±6.88% (78 runs sampled) : 抽样 78 次结果中,上述 ops/sec 的统计误差幅度为 6.88%%。
从数据上看,这里可以得出的性能排比顺序:
shift 》 slice 》splice
这样看起来我们之前选择的 shift 还是性能最强的,当然我们可以从 ECMAScript 规范当中来发现这三者的差异,确实 shift 会更快。
参照 ecma262 文档 shift:https://tc39.es/ecma262/#sec-array.prototype.shift
参照 ecma262 文档 slice:https://tc39.es/ecma262/#sec-array.prototype.slice
参照 ecma262 文档 splice:https://tc39.es/ecma262/#sec-array.prototype.splice
再比较
通过查看 ecma 文档,我们可以轻易的发现为啥 shift 要更快,他们的大体实现如下:
-
shift:做一次循环移位
-
splice:需要新创建一个数组,局部循环一次用于保存删除的数据,然后需要做一次循环移位
-
slice:每次操作都需要创建一个新数组,然后将需要保留的数据移到新数组。
由此看来,shift 会更快一点,也是意料之中,只需要一次循环移位即可。
虽然 slice 和 splice 除了一次循环之外,还有额外开销,但是我们可以采取积累一段数据,再统一删除的方式来减少对数组的执行频率。
修改测试代码如下:
.add("slice#test", function () {
var list = [****];// 预设 30 个节点
let i = 1;
while (i < MAX_COUNT) {
if (i % 20 == 0) {
list = list.slice(20);
}
list.push({ a: i, b: "b" });
i++;
}
})
.add("splice#test", function () {
var list = [****];// 预设 30 个节点
let i = 1;
while (i < MAX_COUNT) {
if (i % 20 == 0) {
list.splice(0, 20);
}
list.push({ a: i, b: "b" });
i++;
}
})
于是,我们再用 benchmark 测试一下,结果如下:
shift#test x 210 ops/sec ±7.33% (81 runs sampled) slice#test x 972 ops/sec ±1.69% (86 runs sampled) splice#test x 696 ops/sec ±0.38% (89 runs sampled)
从这个数据我们可以看出:
slice 》 splice 》 shift
于是最佳方法应该是使用 slice 来更新 msgList 数据。
为啥这样子 slice 又会是最快的呢?如果有疑问,可以再试试阅读 ecma 文档,来体会执行的差异。
直播间上下滑
我们先看下效果,长按小程序二维码,在列表中点击一个进入直播间,然后上滑查看。
小程序直播间怎么做上下滑呢?
relative + animation
这个是H5最通常的做法,京喜H5直播间的上下滑就是这么干的,但是很遗憾,在小程序中行不通,因为实在是 太……卡 了。
scroll-view
scroll-view 滑动自如,我们可否将直播间内容按一个一个 item(height:100vh)顺序排列呢,这样用户将页面滑动到哪就播放哪个直播。
我们确实也尝试了这种方式,但是仍然带来了几个问题:
-
scroll-view 翻页太灵敏了,还带有很强的惯性,效果上难以做到给用户一页一页的翻阅的体验(动态控制里面的 item 效果也不行),
-
滑动到不是整数(100vh)的高度的时候,需要校准归位,响应仍然过慢。
swiper
经过一翻比较,仍然需要采用 swiper 来做直播间上下滑。我在 2019 年 2 月份做小程序视频上下滑的时候,就是采用此方法(
https://blog.youkuaiyun.com/lqyygyss/article/details/87980540
),没想到一年过去了,仍然还是有点卡(不过性能还是有大幅的提升哈~)。
首先,我们来看下布局:
<swiper class="scroll-wrap" circular="{{circular}}" vertical="true" duration="300" bindchange="itemChange" bindanimationfinish="animFinish">
<swiper-item wx:for="{{list}}" wx:key="item" class="scroll-item">
<image class="item-image" src="{{item}}" wx:if="{{index>=currentIndex-1&&index<=currentIndex+1}}"/>
</swiper-item>
<view class="scroll-content" animation="{{animData}}">
……
</view>
</swiper>
这里用一个 swiper 包裹起来,里面按照需要显示的直播列表渲染 swiper-item。非常可喜可贺的是我们在监听用户手势滑动 swiper 的时候,再去实时操作 scroll-content 的 animation ,居然不卡了。当然这里和当前页面只有一个 scroll-content 也有关系。
scroll-content 就是页面的直播所有内容了,节点很庞大,这个 scroll-content 在初始化之后,就一直存在页面中,只是根据滑动到不同的直播间,然后更新里面的内容。
依靠 swiper 来做上下滑,但是真正庞大的直播内容节点,我们永远只有一个实例。
组件间通信
首先,我们来讲下组件通信方式,小程序的组件间通信,通常主要有如下几种方式:
Props & triggerEvent 属性间通信
这种方式是最直接最简单的,相信也是大家使用的最多的方式。
-
优点:简单快捷,容易理解。
-
缺点:通信都需要经过视图层,影响性能。
为啥说这种普遍的方式影响性能呢?
如下摘自小程序文档的一段话:
小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
从这段话,我们总结出来就是:
-
逻辑层(js)和视图层(wxml)之间的交互性能不是那么好,不要太频繁更新。
-
逻辑层(js)和视图层(wxml)的交互还需要通过 evaluateJavascript 实现,需要转换 + 反转编译,不要更新太多内容。
因此,我们如果需要开发高性能的小程序,那么就不得不需要考虑一下了。
emit & on 订阅发布模式通信
这个也是广为使用的一种简单方式,订阅发布者模式,我们在H5中也大量的使用到。
-
优点:容易理解,容易使用,且通信不限组件关系
-
缺点:是无状态的,可能有 BUG
为啥会可能有 BUG 呢?
主要还是订阅发布者模式,通常来说是无状态的,不受制于页面实例,比如京喜小程序,直接使用 getAPP().events 就可以使用了。
如上图的访问路径,那么如果 B 页面的代码发布一个消息通知,那么当前缓存的两个页面 B 都会接受到这个消息,做出响应执行代码,这样子,两个 B 页面的数据就相互污染了。
引起如上消息串了的根本原因是 getAPP().events 是属于小程序级别的消息通知,然后该消息通道又是无状态的,固然可能会被污染。
当然,要解决这个问题,也是可以的,我们可以从如下两个方面来解决:
-
将 events 绑定到页面级实例,那么只会和当前页面实例绑定。
-
给 events 加上作用域,比如注入当前页面的实例,只会是当前页面实例上的监听才会接受到消息。
selectComponent 通信
小程序提供了 selectComponent 方法用来获取子组件的实例,于是我们在父子之间的传递就非常容易了。
-
优点:性能好,直接执行方法
-
缺点:只能父子组件,不能父亲孙组件
如上图所以,A 组件和 B 组件通信都是经过 父pages 来中转的,仅仅是逻辑层的代码调用,所以性能非常优秀,不经过视图层,通信没有啥成本。
在执行 this.selectComponent 的时候,确实也是需要消耗时间的,但是我们只需要把获取到的实例缓存起来即可。
当前京喜直播间主要还是使用 selectComponent 来实现组件间的通信。
定义 corssComponent 方法来实现跨组件通信,然后直接调用即可。
export function crossComponent ({ id, fun, params }) {
if (!id || !fun) return;
const key = id + '_com';
if (!this[key]) {
this[key] = this.selectComponent('#' + id);
}
if (this[key] && typeof this[key][fun] == 'function') {
return this[key][fun](params);
}
// 兜底,拿不到实例的时候,再重复拿实例,保存起来
const self = this;
(function _t (c) {
if (c >= 10) return;
const com = self.selectComponent('#' + id);
if (com) {
self[key] = com;
com && typeof com[fun] == 'function' && com[fun](params);
return;
}
setTimeout(() => {
_t(++c);
}, 200)
})(0);
}
this.crossComponent({id: 'b组件',fun:'add',params:{count:1}});
刚才我们提到了 selectComponent 是不支持子孙组件直接通信,那么我们如果需要子孙组件通信怎么办呢?
其实也不难想到,我们只需要提供保存子孙组件的 crossComponent 的方法即可,我们可以逐级往 子,孙 查找实例,比如 id:"B__b2" 来代表 B 组件的子组件 b2,找到 B 组件的实例,再次 selectComponent 找到 b2,然后将所有的实例保存起来,这样子就可以完全的跨组件通信了。
Canvas 多合一
Canvas 这个东西太好用了,H5 如此,小程序也是一样。
-
深爱的Canvas:功能非常强大,使用非常广泛
-
讨厌的 Canvas:太太太……耗性能了
在小程序 Canvas3d 时代,性能更是瓶颈。当然目前小程序推荐的 Canvas2d 在性能方面改善很多,但是如果页面中有多个地方需要使用到 Canvas 呢?
很不幸,直播间小程序有两个功能需要使用到 Canvas:分享图片的绘制 和 点赞。
初期我们直接页面初始化两个 canvas ,然后发现在低内存手机上就很卡了,甚至直接退出。
解决这个问题,其实可以在页面中只创建一个 Canvas 节点,初始化一次 Canvas 来解决。
欢迎关注我的微信公众号,一起做靠谱前端!
