大量数据的渲染优化

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


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 分钟前
解决 Anki 启动器下载错误的完整指南
前端
im_AMBER22 分钟前
Web 开发 27
前端·javascript·笔记·后端·学习·web
聪明的笨猪猪42 分钟前
Java Redis “缓存设计”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
蓝胖子的多啦A梦1 小时前
低版本Chrome导致弹框无法滚动的解决方案
前端·css·html·chrome浏览器·版本不同造成问题·弹框页面无法滚动
玩代码1 小时前
vue项目安装chromedriver超时解决办法
前端·javascript·vue.js
訾博ZiBo1 小时前
React 状态管理中的循环更新陷阱与解决方案
前端
StarPrayers.1 小时前
旅行商问题(TSP)(2)(heuristics.py)(TSP 的两种贪心启发式算法实现)
前端·人工智能·python·算法·pycharm·启发式算法
聪明的笨猪猪2 小时前
Java Redis “运维”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
一壶浊酒..2 小时前
ajax局部更新
前端·ajax·okhttp
苏打水com2 小时前
JavaScript 面试题标准答案模板(对应前文核心考点)
javascript·面试