首先,当我们要同时渲染大量的数据,例如我用一个for循环去给一个节点增加十万条子项,并打印出时间如下:
let start = new Date();
for (let i = 0; i < total; i++) {
let li = document.createElement('li');
li.innerText = i;
ul.appendChild(li)
}
let end = new Date();
console.log(start-end)
但是这样打印时间是不对的,因为这打印出的只是主线程执行的时间,我们的页面还得进行渲染。
这是由于事件循环机制,页面渲染通常发生在微任务完成后。我们事件循环的机制是这样的(引导面试官问事件循环): 1.Script主线程 2.清空微任务队列 3.执行渲染 4.执行宏任务中的一个任务,并进入下一轮事件循环。
我们的渲染发生在主线程之后,所以我们应该在宏任务中完成定时。
let start = new Date();
for (let i = 0; i < total; i++) {
let li = document.createElement('li');
li.innerText = i;
ul.appendChild(li)
}
let end = new Date();
settimeout(() = > console.log(start-end),0)
我通过浏览器的performance工具(展示实力)查看之后发现,主要是页面的计算样式和渲染占据的时间长(引导面试官问渲染相关知识)。
造成了页面的卡顿,因为js交互为宏任务,无法继续循环。并且一次性渲染过多数据,页面渲染时间长。
因此我们采用分批次递归加入宏任务队列的方法,这样js事件也能从中间穿插,用户仍然能进行交互。
let ul = document.getElementById('container');
let total = 100000;
let once = 20;
let times = Math.ceil(total / once);
let index = 0;
function render() {
for (let i = 0; i < once; i++) {
let li = document.createElement('li');
li.innerText = 1;
ul.appendChild(li);
}
}
function loop() {
setTimeout(() => {
render();
index++;
if (index < times) {
loop();
}
}, 0);
}
loop();
但是这时候还有问题,我们的浏览器刷新页面是60帧,也就是将近16.7ms每次,但是 某部分行为,例如查看滚动位置,(详情看我之前的文章事件循环)。这时候会立即的执行一次渲染。因为他要获取准确的计算结果。这种情况会造成大量的重排,就会引起卡顿。
这时候可以利用 嵌套 requestAnimationFrame 函数,避免中间渲染。提高浏览器的性能。 当嵌套 requestAnimationFrame 函数时候,他会在下一次渲染前执行一次这个回调。
并且他相比我们定时器的好处是定时器可能有几ms的延迟,并且可能在页面刷新时间执行,从而延迟我们的下一帧的刷新。
但是raf 一定会在我们的页面刷新前执行一次,这样能够很好的维护。并且他能随着我们的刷新率主动降级。做到精准同频帧率。
还可以配合上文档碎片(documentFragment),比如跨任务异步操作,移动DOM,能将文档碎片DOM一次性的添加进去。
或者我们也可以通过懒加载的方式,监听用户的onScreen事件来做到懒加载。
终极方案:虚拟列表
虚拟列表就是通过下滑的同时进行列表项的向下偏移。接着根据ScrollTop的位置来决定列表的向下偏移量,达到可以一直滑动的效果。同时根据位置来更新数据项。
每次滑动都会造成样式的重新计算,会造成性能的浪费,可以通过节流函数来进行节流。但是最后一次可能监听不到,需要使用一个多执行一次的高级节流函数。更合理的方案是使用raf来进行计算。
<script setup>
import { ref, computed } from 'vue'
const list = ref([])
for(let i = 0; i < 1000000; i++) {
list.value.push({ id: i, name: `张三${i}` })
}
const itemHeight = 100
const scrollTop = ref(0)
// 真正的节流函数
const throttle = (fn, delay) => {
let lastCall = 0
return function(...args) {
const now = Date.now()
if (now - lastCall >= delay) {
lastCall = now
fn.apply(this, args)
}
}
}
const handleScroll = throttle((e) => {
scrollTop.value = e.target.scrollTop
}, 16) // ~60fps
// 根据滚动位置计算起始索引
const startIndex = computed(() => {
return Math.floor(scrollTop.value / itemHeight)
})
// 计算可见列表
const visibleList = computed(() => {
return list.value.slice(startIndex.value, startIndex.value + 5)
})
// 计算所有项的总高度
const totalHeight = computed(() => {
return list.value.length * itemHeight
})
// 计算列表位置偏移
const startOffset = computed(() => {
return startIndex.value * itemHeight
})
</script>
<template>
<div class="container">
<div
class="scroll-container"
:style="{
overflow: 'auto',
height: '400px',
width: '120px',
border: '1px solid #ccc',
position: 'relative'
}"
@scroll="handleScroll"
>
<!-- 撑开滚动区域的占位符 -->
<div :style="{ height: totalHeight + 'px' }"></div>
<!-- 列表项容器 - 整体移动 -->
<div
class="list-items-container"
:style="{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${startOffset}px)`
}"
>
<div
v-for="item in visibleList"
:key="item.id"
:style="{
height: itemHeight + 'px',
width: '100%',
backgroundColor: 'red',
border: '2px solid black',
boxSizing: 'border-box',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}"
>
{{ item.name }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>
-
虚拟列表与传统渲染方式相比,具体能提升哪些性能指标?
只需要渲染可视区域元素,大大减少了DOM渲染,首屏加载时间减少
-
IntersectionObserver API可以用于优化虚拟列表
能够精确的监听进入视口的时间,这样不用任何滚动事件都去触发onScroll事件。
-
虚拟列表当快速或者跳跃式滑动时出现的白屏如何解决
增加渲染缓冲区 !!! 跳跃滚动不节流。使用raf代替节流。 假如出现极大的跳跃式滚动的话,单独处理跳跃逻辑,占位图或者骨架屏。 硬件加速 translateZ(0);
-
为什么渲染选择20条数据?
单条数据渲染约0.5-1ms,20条数据通常能在10-15ms内完成,太小(如5条):需要更多渲染批次,完成总渲染时间长,太大(如50条)可能导致单次渲染阻塞时间过长,影响页面与交互流畅度。
-
你使用了 transform 来偏移列表,而不是改变 top 值,这两种方式在性能上有什么区别?为什么前者更好?
transform可能会进行GPU的加速,并且transform不会引起重排。top是在layout布局阶段干的事。
- 在虚拟列表实现中,当列表项高度不一致时,你的算法需要如何调整?能否详细描述这种情况下的实现思路?
根据高度计算偏移量,通过总高度决定渲染数量。计算存储每个项起始位置。