前端技术分享:页面性能优化问题复盘

项目背景

在 code_pc 项目中,前端需要使用 rrweb 对老师教学内容进行录制,学员可以进行录制回放。为减小录制文件体积,当前的录制策略是先录制一次全量快照,后续录制增量快照,录制阶段实际就是通过 MutationObserver 监听 DOM 元素变化,然后将一个个事件 push 到数组中。

为了进行持久化存储,可以将录制数据压缩后序列化为 JSON 文件。老师会将 JSON 文件放入课件包中,打成压缩包上传到教务系统中。学员回放时,前端会先下载压缩包,通过 JSZip 解压,取到 JSON 文件后,反序列化再解压后,得到原始的录制数据,再传入 rrwebPlayer 实现录制回放。

发现问题

在项目开发阶段,测试录制都不会太长,因此录制文件体积不大(在几百 kb),回放比较流畅。但随着项目进入测试阶段,模拟长时间上课场景的录制之后,发现录制文件变得很大,达到 10-20 M,QA 同学反映打开学员回放页面的时候,页面明显卡顿,卡顿时间在 20s 以上,在这段时间内,页面交互事件没有任何响应。

页面性能是影响用户体验的主要因素,对于如此长时间的页面卡顿,用户显然是无法接受的。

问题排查

经过组内沟通后得知,可能导致页面卡顿的主要有两方面因素:前端解压 zip 包,和录制回放文件加载。同事怀疑主要是 zip 包解压的问题,同时希望我尝试将解压过程放到 worker 线程中进行。那么是否确实如同事所说,前端解压 zip 包导致页面卡顿呢?

3.1 解决 Vue 递归复杂对象引起的耗时问题

对于页面卡顿问题,首先想到肯定是线程阻塞引起的,这就需要排查哪里出现长任务。

所谓长任务是指执行耗时在 50ms 以上的任务,大家知道 Chrome 浏览器页面渲染和 V8 引擎用的是一个线程,如果 JS 脚本执行耗时太长,就会阻塞渲染线程,进而导致页面卡顿。

对于 JS 执行耗时分析,这块大家应该都知道使用 performance 面板。在 performance 面板中,通过看火焰图分析 call stack 和执行耗时。火焰图中每一个方块的宽度代表执行耗时,方块叠加的高度代表调用栈的深度。

按照这个思路,我们来看下分析的结果:
[外链图片转存中…(img-nfpBonig_convert/61cbb6f6f8d1d2e7b232fc791ca00e4c.png)
可以看到,replayRRweb 显然是一个长任务,耗时接近 18s ,严重阻塞了主线程。

而 replayRRweb 耗时过长又是因为内部两个调用引起的,分别是左边浅绿色部分和右边深绿色部分。我们来看下调用栈,看看哪里哪里耗时比较严重:
[外链图片转存中…(img-Gthgovert/0302be66a0743642bbb24c0111ba5eb6.png)
熟悉 Vue 源码的同学可能已经看出来了,上面这些耗时比较严重的方法,都是 Vue 内部递归响应式的方法(右边显示这些方法来自 vue.runtime.esm.js)。

为什么这些方法会长时间占用主线程呢?在 Vue 性能优化中有一条:不要将复杂对象丢到 data 里面,否则会 Vue 会深度遍历对象中的属性添加 getter、setter(即使这些数据不需要用于视图渲染),进而导致性能问题。

那么在业务代码中是否有这样的问题呢?我们找到了一段非常可疑的代码

export default {
   
  data() {
   
    return {
   
      rrWebplayer: null
    }
  },
  mounted() {
   
    bus.$on("setRrwebEvents", (eventPromise) => {
   
      eventPromise.then((res) => {
   
        this.replayRRweb(JSON.parse(res));
      })
    })
  }, methods: {
   
    replayRRweb(eventsRes) {
   
      this.rrWebplayer = new rrwebPlayer({
   
        target: document.getElementById('replayer'),
        props: {
   
          events: eventsRes,
          unpackFn: unpack,
          // ...
        }
      })
    }
  }
}

在上面的代码中,创建了一个 rrwebPlayer 实例,并赋值给 rrWebplayer 的响应式数据。在创建实例的时候,还接受了一个 eventsRes 数组,这个数组非常大,包含几万条数据。

这种情况下,如果 Vue 对 rrWebplayer 进行递归响应式,想必非常耗时。因此,我们需要将 rrWebplayer 变为 Non-reactive data(避免 Vue 递归响应式)。

转为 Non-reactive data,主要有三种方法

数据没有预先定义在 data 选项中,而是在组件实例 created 之后再动态定义 this.rrwebPlayer

你可能感兴趣的:(有道技术团队,前端,性能优化,javascript)