大量数据的渲染优化

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


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 提供了异步渲染的能力,可以避免主线程阻塞。
  • 虚拟列表是一种高效的解决方案,通过仅渲染可见区域内的数据,极大提高了性能。
相关推荐
究极无敌暴龙战神X2 分钟前
哈希表 - 两个数组的交集(集合、数组) - JS
前端·javascript·散列表
前端御书房5 分钟前
基于 Trae 的超轻量级前端架构设计与性能优化实践
前端·性能优化
南部余额10 分钟前
playwright解决重复登录问题,通过pytest夹具自动读取storage_state用户状态信息
前端·爬虫·python·ui·pytest·pylawright
前端与小赵21 分钟前
webpack和vite之间的区别
前端·webpack·vite
zy01010124 分钟前
React受控表单绑定
前端·javascript·react.js·mvvm·双向数据绑定
百锦再25 分钟前
React编程的核心概念:数据流与观察者模式
前端·javascript·vue.js·观察者模式·react.js·前端框架·ecmascript
2401_8724878826 分钟前
网络安全之前端学习(css篇2)
前端·css·学习
SuperYing37 分钟前
前端候选人突围指南:让面试官主动追着要简历的五大特质(个人总结版)
前端·面试
前端双越老师40 分钟前
我的编程经验与认知
前端
linweidong1 小时前
前端Three.js面试题及参考答案
前端·javascript·vue.js·typescript·前端框架·three.js·前端面经