在这篇文章中,我们将探讨如何高效地渲染大量数据。无论是实现页面滚动,还是处理大量列表项的渲染,优化性能是至关重要的。我们将通过几个不同的方案来演示如何解决渲染性能问题,减少卡顿和闪屏现象。
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. 总结
在渲染大量数据时,我们可以选择不同的优化策略:
- 直接渲染适用于小数据量,但对于大数据量会导致性能瓶颈。
requestAnimationFrame
和setTimeout
提供了异步渲染的能力,可以避免主线程阻塞。- 虚拟列表是一种高效的解决方案,通过仅渲染可见区域内的数据,极大提高了性能。