编程 前端如何一次性渲染十万条数据?

2024-11-19 05:08:27 +0800 CST views 467

前端如何一次性渲染十万条数据?

前言

当面试官问:“给你十万条数据,你会怎么办?”这时我们该如何应对呢?

在实际的 Web 开发中,有时需要展示大量数据,比如用户评论、商品列表等。如果一次性渲染太多的数据(如100,000条),会导致浏览器卡顿,用户体验变差。下面从一个简单的例子开始,逐步改进代码,最终使用现代框架的虚拟滚动技术来解决这一问题。

正文

最直接的方法

下面是最直接的方法,一次性创建所有的列表项并添加到 DOM 树中。

<!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');
        const total = 100000;
        let now = Date.now();
        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);
        });
    </script>
</body>
</html>

代码解释:

该方法简单直接,但由于一次性将所有数据渲染到 DOM 中,导致浏览器需要较长时间渲染页面,出现页面卡顿或白屏的情况。

setTimeout 分批渲染

为了避免一次性操作引起的卡顿,可以使用 setTimeout 将创建和添加操作分散到多个时间点,每次渲染一部分数据。

<!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');
        const total = 100000;
        let once = 20;
        let index = 0;

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

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

代码解释:

通过将渲染任务分批执行,减少一次性渲染的压力,避免页面卡顿或白屏的问题。

使用 requestAnimationFrame

相比 setTimeoutrequestAnimationFrame 是更优秀的解决方案,它能保证操作在浏览器每一帧刷新时进行。

<!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');
        const total = 100000;
        let once = 20;
        let index = 0;

        function loop(curTotal, curIndex) {
            let pageCount = Math.min(once, curTotal);
            requestAnimationFrame(() => {
                for (let i = 0; i < pageCount; i++) {
                    let li = document.createElement('li');
                    li.innerText = curIndex + i + ':' + ~~(Math.random() * total);
                    ul.appendChild(li);
                }
                if (curTotal > pageCount) {
                    loop(curTotal - pageCount, curIndex + pageCount);
                }
            });
        }

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

代码解释:

requestAnimationFrame 可以让渲染更加流畅,避免了 setTimeout 可能出现的卡顿问题。

使用文档碎片(requestAnimationFrame + DocumentFragment

使用 DocumentFragment 可以将所有节点暂时存放在内存中,减少 DOM 操作次数,进一步提升性能。

<!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');
        const total = 100000;
        let once = 20;
        let index = 0;

        function loop(curTotal, curIndex) {
            let fragment = document.createDocumentFragment();
            let pageCount = Math.min(once, curTotal);
            requestAnimationFrame(() => {
                for (let i = 0; i < pageCount; i++) {
                    let li = document.createElement('li');
                    li.innerText = curIndex + i + ':' + ~~(Math.random() * total);
                    fragment.appendChild(li);
                }
                ul.appendChild(fragment);
                if (curTotal > pageCount) {
                    loop(curTotal - pageCount, curIndex + pageCount);
                }
            });
        }

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

代码解释:

DocumentFragment 将多个 DOM 操作合并为一次提交,减少回流与重绘,进一步提高渲染性能。

使用虚拟滚动(Virtual Scrolling)

虚拟滚动只渲染当前可视区域内的数据,当用户滚动时,动态替换数据。这是处理大数据集的最佳实践。

<template>
  <div class="app">
    <virtualList :listData="data"></virtualList>
  </div>
</template>

<script setup>
import virtualList from './components/virtualList.vue'

const data = [];
for (let i = 0; i < 100000; i++) {
  data.push({ id: i, value: i });
}
</script>

<style scoped>
.app {
  height: 400px;
  width: 300px;
  border: 1px solid #000;
}
</style>
<template>
  <div ref="listRef" class="infinite-list-container" @scroll="scrollEvent">
    <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
    <div class="infinite-list" :style="{ transform: getTransform }">
      <div
        class="infinite-list-item"
        v-for="item in visibleData"
        :key="item.id"
        :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }"
      >
        {{ item.value }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, ref, reactive, onMounted } from 'vue';

const props = defineProps({
  listData: Array,
  itemSize: { type: Number, default: 50 }
});

const state = reactive({
  screenHeight: 0,
  startOffset: 0,
  start: 0,
  end: 0
});

const visibleCount = computed(() => Math.ceil(state.screenHeight / props.itemSize));
const visibleData = computed(() => props.listData.slice(state.start, Math.min(state.end, props.listData.length)));
const listHeight = computed(() => props.listData.length * props.itemSize);
const getTransform = computed(() => `translateY(${state.startOffset}px)`);

const listRef = ref(null);

onMounted(() => {
  state.screenHeight = listRef.value.clientHeight;
  state.end = state.start + visibleCount.value;
});

const scrollEvent = () => {
  const scrollTop = listRef.value.scrollTop;
  state.start = Math.floor(scrollTop / props.itemSize);
  state.end = state.start + visibleCount.value;
  state.startOffset = scrollTop - (scrollTop % props.itemSize);
};
</script>

<style scoped>
.infinite-list-container {
  height: 100%;
  overflow: auto;
  position: relative;
}

.infinite-list-phantom {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
}

.infinite-list {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  text-align: center;
}

.infinite-list-item {
  border-bottom: 1px solid #eee;
  box-sizing: border-box;
}
</style>

代码解释:

虚拟滚动的核心思想是只渲染当前可视区域的数据,大大减少渲染

的 DOM 数量,提升性能。

总结

通过上述方法,从最简单的 DOM 操作到使用虚拟滚动技术,我们逐步提升了渲染大数据量时的性能。使用虚拟滚动是最佳实践,尤其是在处理非常大规模的数据时。

推荐文章

php机器学习神经网络库
2024-11-19 09:03:47 +0800 CST
Hypothesis是一个强大的Python测试库
2024-11-19 04:31:30 +0800 CST
Boost.Asio: 一个美轮美奂的C++库
2024-11-18 23:09:42 +0800 CST
避免 Go 语言中的接口污染
2024-11-19 05:20:53 +0800 CST
go错误处理
2024-11-18 18:17:38 +0800 CST
利用Python构建语音助手
2024-11-19 04:24:50 +0800 CST
CSS 媒体查询
2024-11-18 13:42:46 +0800 CST
如何将TypeScript与Vue3结合使用
2024-11-19 01:47:20 +0800 CST
JavaScript设计模式:观察者模式
2024-11-19 05:37:50 +0800 CST
底部导航栏
2024-11-19 01:12:32 +0800 CST
2025,重新认识 HTML!
2025-02-07 14:40:00 +0800 CST
Go 单元测试
2024-11-18 19:21:56 +0800 CST
Grid布局的简洁性和高效性
2024-11-18 03:48:02 +0800 CST
PHP 8.4 中的新数组函数
2024-11-19 08:33:52 +0800 CST
免费常用API接口分享
2024-11-19 09:25:07 +0800 CST
支付轮询打赏系统介绍
2024-11-18 16:40:31 +0800 CST
CSS 中的 `scrollbar-width` 属性
2024-11-19 01:32:55 +0800 CST
如何实现生产环境代码加密
2024-11-18 14:19:35 +0800 CST
程序员茄子在线接单