大量数据的渲染优化

在这篇文章中,我们将探讨如何高效地渲染大量数据。无论是实现页面滚动,还是处理大量列表项的渲染,优化性能是至关重要的。我们将通过几个不同的方案来演示如何解决渲染性能问题,减少卡顿和闪屏现象。


1. 直接循环渲染:性能问题

直接循环渲染的方式最简单,但也存在一些性能问题,特别是在处理大量数据时。以下是一个简单的例子:

示例 1:直接渲染

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <ul id="container"></ul>

  <script>
    let ul = document.getElementById('container');
    let now = Date.now();
    let total = 10000;
    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);
  </script>
</body>
</html>

问题:

  • 直接将所有数据添加到DOM中会导致页面卡顿,特别是当数据量很大时,渲染时间会非常长。

2. 使用定时器

使用 setTimeout 来分批渲染数据。通过将渲染操作放到 setTimeout 中,我们可以确保每次渲染只执行一小部分,避免主线程阻塞。

示例 3:使用 setTimeout

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <ul id="container"></ul>

  <script>
    let ul = document.getElementById('container');
    let total = 10000;
    let once = 20;
    let page = total / once;
    let index = 0;

    function render(curTotal, curIndex) {
      if (curTotal <= 0) return;
      
      let pageCount = Math.min(curTotal, once);
      setTimeout(() => {
        for (let i = 0; i < pageCount; i++) {
          let li = document.createElement('li');
          li.innerText = curIndex * once + i;
          ul.appendChild(li);
        }
        render(curTotal - pageCount, curIndex + 1);
      }, 0);
    }

    render(total, index);
  </script>
</body>
</html>

优势:

  • 使用 setTimeout 将渲染操作分批处理,使得渲染过程不会阻塞主线程,但也存在定时器与屏幕刷新不同步的问题。

问题:

  • 定时器的执行时间与屏幕刷新时间不同,会导致显示不流畅,卡顿闪屏。

3. 使用requestAnimationFrame:改进渲染

为了避免一次性渲染导致的页面卡死,可以通过定时器将渲染操作分批执行。这样可以让浏览器有机会更新UI,避免长时间占用主线程。同时使用requestAnimationFrame保证与屏幕刷新时间同步,优化显示。

示例 2:使用 requestAnimationFrame

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <ul id="container"></ul>

  <script>
    let ul = document.getElementById('container');
    let total = 10000;
    let once = 20;
    let page = total / once;
    let index = 0;

    function render(curTotal, curIndex) {
      if (curTotal <= 0) return;

      let pageCount = Math.min(curTotal, once);
      requestAnimationFrame(() => { // 屏幕刷新时执行,保证执行和屏幕刷新同步,不出现卡顿闪屏

        let fragment = document.createDocumentFragment();
        for (let i = 0; i < pageCount; i++) {
          let li = document.createElement('li');
          li.innerText = curIndex * once + i;
          fragment.appendChild(li);
        }
        ul.appendChild(fragment);
        render(curTotal - pageCount, curIndex + 1);
      });
    }

    render(total, index);
  </script>
</body>
</html>

优势:

  • requestAnimationFrame 可以将渲染操作与浏览器的刷新帧同步,避免了定时器不确定的执行时机,减少了卡顿和闪屏现象。

5. 虚拟列表:优化大数据渲染

虚拟列表是一种针对大数据量的优化方法,也是常用的一种手段,它只渲染可见区域内的元素。通过计算可视区域的高度和每个项的高度,可以只渲染用户当前能看到的数据,极大提高性能。

示例 4:虚拟列表

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    .v-scroll {
      width: 200px;
      height: 400px;
      overflow: auto;
      border: 1px solid #000;
      margin: 30px 0 0 30px;
    }

    li {
      height: 40px;
      text-align: center;
      line-height: 40px;
      border-bottom: 1px solid #a5a0a0;
      box-sizing: border-box;
    }
  </style>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>

<body>
  <div id="app">
    <div class="v-scroll" ref="scrollRef" @scroll="doScroll">
      <ul :style="blankStyle" style="height: 100%;">
        <li v-for="i in tempData" :key="i">{{i}}</li>
      </ul>
    </div>
  </div>

  <script>
    // 截流函数
    function throttle(fn, delay) {
      let timer = Date.now();
      return function () {
        let current = Date.now();
        if (current - timer > delay) {
          fn.apply(this, arguments);
          timer = current;
        }
      }
    }

    const { createApp, ref, onMounted, computed } = Vue;

    createApp({
      setup() {
        function getAllData() {
          let data = [];
          for (let i = 1; i <= 10000; i++) {
            data.push(i);
          }
          return data;
        }

        const allData = ref(getAllData()); // 获取到所有的数据
        const scrollRef = ref(null); // 可视区域的 ref
        const boxHeight = ref(0); // 可视区域的高度
        const itemHeight = ref(40); // 每个 li 的高度
        const itemNum = ref(0); // 可视区域可以展示的数据量
        const startIndex = ref(0); // 开始的索引
        const endIndex = computed(() => { // 结束的索引
          let index = startIndex.value + itemNum.value * 2;
          if (!allData.value[index]) {
            index = allData.value.length - 1;
          }
          return index;
        });

        // 获取可视区域的高度
        onMounted(() => {
          boxHeight.value = scrollRef.value.clientHeight;
          itemNum.value = Math.ceil(boxHeight.value / itemHeight.value) + 2;
        });

        const tempData = computed(() => { // 被展示数据
          let index = 0;
          if (startIndex.value <= itemNum.value) {
            index = 0;
          } else {
            index = startIndex.value - itemNum.value;
          }
          return allData.value.slice(index, endIndex.value + 1); // allData[index] 为开始的数据
        });

        // 监听滚动事件 修改开始索引
        const doScroll = throttle(() => {
          console.log(startIndex.value, endIndex.value);

          const index = ~~(scrollRef.value.scrollTop / itemHeight.value);
          if (index === startIndex.value) return;
          startIndex.value = index;
        }, 200);

        const blankStyle = computed(() => {
          let index = 0;
          if (startIndex.value <= itemNum.value) {
            index = 0;
          } else {
            index = startIndex.value - itemNum.value;
          }
          return {
            paddingTop: index * itemHeight.value + 'px', // 划出屏幕的数据的高度
            paddingBottom: (allData.value.length - endIndex.value - 1) * itemHeight.value + 'px' // 剩下的数据高度
          };
        });

        return {
          allData,
          scrollRef,
          tempData,
          doScroll,
          blankStyle
        };
      }
    }).mount('#app');
  </script>
</body>

</html>

优势:

  • 虚拟列表只渲染用户可见的部分,大大减少了DOM节点的数量,提高渲染效率。
  • 使用 throttle 控制滚动事件的频率,防止过于频繁地触发渲染。

6. 总结

在渲染大量数据时,我们可以选择不同的优化策略:

  • 直接渲染适用于小数据量,但对于大数据量会导致性能瓶颈。
  • requestAnimationFramesetTimeout 提供了异步渲染的能力,可以避免主线程阻塞。
  • 虚拟列表是一种高效的解决方案,通过仅渲染可见区域内的数据,极大提高了性能。
相关推荐
七灵微2 小时前
ES6入门---第三单元 模块三:async、await
前端·javascript·es6
七灵微4 小时前
ES6入门---第二单元 模块五:模块化
前端·ecmascript·es6
m0_616188495 小时前
vue3 - keepAlive缓存组件
前端·vue.js·缓存
lh_12546 小时前
Uni-app 组件使用
前端·javascript·uni-app
Kx…………6 小时前
Day3:设置页面全局渐变线性渐变背景色uniapp壁纸实战
前端·学习·uni-app·实战·项目
试着6 小时前
【AI面试准备】元宇宙测试:AI+低代码构建虚拟场景压力测试
人工智能·低代码·面试
Q_Boom6 小时前
前端跨域问题怎么在后端解决
java·前端·后端·spring
搬砖工程师Cola6 小时前
<Revit二次开发> 通过一组模型线构成墙面,并生成墙。Create(Document, IList.Curve., Boolean)
java·前端·javascript
林十一npc6 小时前
Fiddler抓取APP端,HTTPS报错全解析及解决方案(一篇解决常见问题)
android·前端·网络协议·https·fiddler·接口测试
小妖6667 小时前
4个纯CSS自定义的简单而优雅的滚动条样式
前端·javascript·css