以下文章来源于ELab团队 ,作者ELab.zhengyb
性能优化一直是前端领域老生常谈的问题,系统的性能以及稳定性很大程度上决定着产品的用户体验以及产品所能达到的高度。而tob和toc系统又有着不同的业务场景,性能优化也有着不用的着力点。本文从笔者的视角出发,结合自己针对一个tob系统的性能优化实践去剖析一些大家可能共同关注的点,争取可以以小见大。
我所在的团队是一个涉及业务比较复杂的的教育前端团队,而谈及在线教育,始终绕不开在线讲义,在线课件这一关,我们所负责的业务旨在提供完善的在线课件解决方案:
我们输出的产品主要包括 编辑器
和 渲染器
两部分。
编辑器
除了提供基础的课件编辑制作能力外,还提供了组装各类教育资源的能力,这些教育资源包括互动题、cocos、pdf、ppt等。
渲染器
除了提供通用渲染器来支持基础课件的渲染以外,还支持接入各类教育资源的渲染器,来支持教育资源的渲染。
关于数据结构,大致数据结构如下所示,类似ppt的数据结构,每一页单页课件是一个page,每页课件上中的文字图片音频视频都是一个节点,这些课件页以及节点都是以数组的形式来维护。
{
pages: [
data: {
nodes: [
'text', 'image', 'video', 'staticQuestion'...
]
}
]
}
简单了解业务之后我们才能结合具体的场景讨论性能优化过程中遇到的问题。
我们的项目规划一般按照双月来制定目标,34双月我们成立课件性能优化专项,双月目标是明显提升用户体验。
下面我会从遇到的具体case入手,来聊一聊我们是如何解决这些问题的。
原因分析
我们课件系统的数据依然采用了序列化数据存储(未分页),而我们打开编辑器时,会发请求拿到课件的所有内容,课件内容也会一股脑儿渲染在页面上,这样带来的结果就是页面的性能非常受课件体量的制约,随着课件内容越来越多,课件页面达到100页以上时,系统的性能就已经到达了瓶颈,具体表现为点击切换课件页卡顿以及列表页滚动卡顿。
我们在列表的vue组件的updated 生命周期中添加了一个 log 查看组件渲染次数:
updated() {
// 查看该组件更新了多少次,勿删
console.log("%c left viewer rerender", 'color: red;');
},
Vue 的 updated官网这样解释道:
由于数据更改导致的虚拟 DOM 重新渲染和打补丁。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用 计算属性 或 watcher 取而代之。
https://cn.vuejs.org/v2/api/#updated
于是我们发现点击整个单个课件页时,整个左侧列表都重新渲染,而每个课件页中的log也会执行,而且会渲染三次。
我们初步判断当点击单页时,组件执行了多余的render,而在重新渲染之前虚拟dom的计算阻塞了单线程,导致ui假死。虽然Vue内部对虚拟dom的计算做了很多优化,但是在这个案例中我们看到,课件体量大时,单线程依然会阻塞,我们通过performance可以进一步证明我们的猜想。
通过 perfermance可以看到,一次点击事件的处理时间达到4.16s,这一桢的时间是4500ms,在这四秒多时间内,浏览器是没有任何响应的,而通过观察我们发现这段时间耗时的操作就是Vue的虚拟dom计算过程,在Bottom-up中也可以看到,耗时操作vue removeSub移除依赖的操作,还有虚拟dom patch node 的计算,这个过程是为了合并更新,这个计算堆积起来就非常耗时。
排查到这里我将原因归结为组件太多,不必要的更新太多,我们去查看了一下页面节点数量
200页的课件全部渲染,页面节点已经到达了3w之多,而每次交互更新量巨大,浏览器重绘的压力也比较大(虽然这个时间比js计算还是少很多)。
经过以上的排查,我们总结原因为:Vue的数据侦听应该更新变化了的dom,但是我们点击某个课件页时,由于处理Vuex的数据流的方式不太合理,使得许多组件依赖了本不需要的数据,导致Vue判断组件需要重新渲染。其次我们的页面结构过于复杂,没有做动态渲染或者feed流类似的分片加载策略,浪费了很多资源。
解决方案
基于以上的原因分析,我们尝试了比较多的方案。其中由于Vuex数据流不合理带来的过多rerender,由于项目过于复杂,涉及到了互动题编辑器和模版编辑器,数据流的改动风险较大,而且收益不一定明显。于是在3-4月的优化中,我们没有动现有的状态管理,而是在当前基础上,力求减少每次操作的计算量和渲染量,也就是在不合理的方案下去缓解用户体验问题。我们的思路聚焦在:课件列表需要动态加载,页面节点越少,挂载的组件越简单,Vue的计算越快,浏览器渲染的速度也越快。 于是我们做了以下尝试:
IntersectionObserver
借鉴图片懒加载的方式,我们通过浏览器的 IntersectionObserver 进行 dom 的监听实现课件页的懒加载
// 在需要懒加载的节点上添加ref属性为“containerNeedLazyLoad”
// 将控制是否进行加载的boolean变量命名为“elementNeedLoad”
export default {
data() {
return {
elementNeedLoad: false,
elementNeedVisible: false
};
},
mounted() {
const target = this.$refs.containerNeedLazyLoad;
const intersectionObserver = this.lazyLoadObserver(target);
intersectionObserver.observe(target);
},
methods: {
lazyLoadObserver(target) {
return new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry