性能优化篇之深入虚拟列表

1,732 阅读2分钟

前端业务遇到的长列表

前端的业务开发中会遇到一些数据量大且无法使用分页方式来加载的列表,我们常把他成为长列表。那既然是长列表了,如果第一次就完整渲染的话,一是业务上不允许:太长了。二是性能也是非常低的。

长列表解决方案

  • 懒加载: 就是常见的无限滚动,每次只渲染出一部分,随着滚动,剩余部分出现在可视区域,继续渲染。
  • 可见区域(也就是做成虚拟列表): 就是在固定可是窗口中,右侧有滚动条。固定窗口的高度就是可视区域。

问: 虚拟列表和无线滚动有什么区别呢?

无限滚动就是一般常见的触底加载,滚动到页面底部加载更多数据,但是当数据量上来之后,达到千万数量级别,浏览器没法承载这么多的节点渲染,肯定会卡顿甚至崩溃,这个时候就可以使用虚拟列表,通过计算滚动视窗,每次只渲染可见屏幕部分节点,超出屏幕的不可见范围用内填充 padding 代替,对于浏览器来说无论你滚动到什么位置,渲染的都是屏幕范围内的节点,这样就不会有性能负担了。

例子: 插入一万条li数据后,性能分析。

document.getElementById('button').addEventListener('click',function(){
    // 记录任务开始时间
    let now = Date.now();
    const total = 10000;
    let ul = document.getElementById('container');
    // 将数据插入容器中
    for (let i = 0; i < total; i++) {
        let li = document.createElement('li');
        li.innerText = ~~(Math.random() * total)
        ul.appendChild(li);
    }
    console.log('JS运行时间:',Date.now() - now);
    setTimeout(()=>{
      console.log('总运行时间:',Date.now() - now);
    },0)

    // print JS运行时间: 38
    // print 总运行时间: 957 
  })

image.png

Event(click) : 40.84ms
Recalculate Style : 105.08ms
Layout : 731.56ms
Update Layer Tree : 58.87ms
Paint : 15.32ms

有的同学可能会有疑问: 问什么你这样卡节点就能说是JS运行时间和总运行时间

这里考点问: 浏览器的event loop

  • js引擎所管理的执行栈中清空,并且所有的微任务事件全部执行完,触发渲染线程对页面进行渲染。
  • 第二个console.log(),触发时间:在渲染完成,下一次Event Loop中执行的。

从图中我们可以看到,recalculate style 和 layout消耗的时间最长

方案: 

  1. 我们最好是使用一些现成的虚拟列表组件来对长列表进行优化,比较常见的有 react-virtualized 和 react-tiny-virtual-list 这两个组件,使用他们可以有效地对你的长列表进行优化。

  2. 原生实现 预备知识:

网页可见区域高:document.body.clientHeight

网页正文全文高:document.body.scrollHeight
网页可见区域高(包括边线的高):document.body.offsetHeight
网页被卷去的高:document.body.scrollTop

屏幕分辨率高:window.screen.height
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>简单实现虚拟列表</title>
</head>
<style>
  .list-view {
    height: 400px;
    overflow: auto;
    position: relative;
    border: 1px solid #aaa;
  }

  .list-view-phantom {
    /* 使用不可见区域,撑起这个列表,让列表的滚动条出现 */
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: -1;
  }

  .list-view-content {
    left: 0;
    right: 0;
    top: 0;
    position: absolute;
  }

  .list-view-item {
    padding: 5px;
    color: #666;
    line-height: 30px;
    box-sizing: border-box;
  }

  [v-cloak] {
    display: none;
  }
</style>

<body>
  <div id="app" v-cloak>
    <div class="list-view" ref="scrollBox" @scroll="handleScroll">
      <div class="list-view-phantom" :style="{
                       height: contentHeight
                    }"></div>
      <div ref="content" class="list-view-content">
        <div class="list-view-item" :style="{
                        height: itemHeight + 'px'
                      }" v-for="item in visibleData">
          {{ item }}
        </div>
      </div>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    new Vue({
      el: "#app",
      computed: {
        contentHeight() {
          return this.data.length * this.itemHeight + "px";
        },
      },
      mounted() {
        this.updateVisibleData();
      },
      data() {
        return {
          data: new Array(100).fill(1),
          itemHeight: 30,
          visibleData: [],
        };
      },
      methods: {
        updateVisibleData(scrollTop = 0) {
          console.log('scrollTop', scrollTop)
          const visibleCount = Math.ceil(
            this.$refs.scrollBox.clientHeight / this.itemHeight
          );
          const start = Math.floor(scrollTop / this.itemHeight);
          const end = start + visibleCount;
          this.visibleData = this.data.slice(start, end);
          this.$refs.content.style.webkitTransform = `translate3d(0, ${start * this.itemHeight
            }px, 0)`;
        },
        handleScroll() {
          const scrollTop = this.$refs.scrollBox.scrollTop;
          this.updateVisibleData(scrollTop);
        },
      },
    });
  </script>
</body>

</html>

待完善

  • 动态列表
  • 含有图片的监听列表,运用实验API ResizeObserver