面试官:同时插入100000个元素怎么让页面不卡顿
优化前写法
首先我们来看下面的一段,点击按钮后,循环100000次,每次都插入一个元素,并且插入区域上方还有一个小球在滚动,在插入的过程中我们可以观察小球的动画是否出现卡顿
<template>
<h3>小球左右上下循环移动动画</h3>
<div class="container">
<div id="ball" class="ball"></div>
</div>
<h3>前端性能优化之</h3>
<el-button type="primary" @click='addBall'>加载10000个小球</el-button>
<div id='ballBox' class='ballBox'></div>
</template>
<script setup>
onMounted(() => {
const ball = document.getElementById('ball')
const container = document.querySelector('.container')
// 获取容器的宽高
const containerWidth = container.offsetWidth
const containerHeight = container.offsetHeight
// 小球的初始位置和速度
let ballX = 0
let ballY = 0
let speedX = 2 // 水平速度
let speedY = 2 // 垂直速度
// 动画函数
function moveBall() {
// 更新小球的位置
ballX += speedX
ballY += speedY
// 检测边界碰撞并反转方向
if (ballX <= 0 || ballX + ball.offsetWidth >= containerWidth) {
speedX = -speedX
}
if (ballY <= 0 || ballY + ball.offsetHeight >= containerHeight) {
speedY = -speedY
}
// 应用新的位置
ball.style.left = `${ballX}px`
ball.style.top = `${ballY}px`
// 循环调用动画
requestAnimationFrame(moveBall)
}
// 启动动画
moveBall()
})
function addBall(){
let ballBox = document.getElementById('ballBox')
const tasks = Array.from({ length: 100000 }, (_, i) => {
return () => {
let ball = document.createElement('div')
ball.innerText = i + 1
ballBox.appendChild(ball)
}
})
for (let i = 0; i < tasks.length; i++) {
tasks[i]()
}
}
</script>
<style scoped>
.container {
position: relative;
width: 400px;
height: 300px;
border: 2px solid #ccc;
background-color: #f5f5f5;
overflow: hidden; /* 防止小球超出容器 */
}
.ball {
position: absolute;
width: 30px;
height: 30px;
background-color: red;
border-radius: 50%; /* 圆形 */
}
</style>
目前的效果
可以看到点击后小球明显的出现了一段时间的卡顿才恢复正常
使用DocumentFragment减少DOM操作
DocumentFragment
是一个轻量级的文档对象,可以作为一个临时的容器来存储一组节点。它的主要优点是可以减少对 DOM 的多次操作,从而提高性能。我们可以将多个节点添加到 DocumentFragment
中,然后一次性将 DocumentFragment
添加到 DOM 中。从而较少浏览器的重绘和回流
改写代码如下
function addBall() {
// 获取要添加小球的容器元素
let ballBox = document.getElementById('ballBox')
// 创建一个 DocumentFragment,用于批量添加小球
const fragment = document.createDocumentFragment()
// 创建一个包含 100000 个任务的数组,每个任务用于创建一个小球并添加到 fragment 中
const tasks = Array.from({ length: 100000 }, (_, i) => {
return () => {
// 创建一个 div 元素表示小球
let ball = document.createElement('div')
// 设置小球的文本内容为其序号
ball.innerText = i + 1
// 将小球添加到 fragment 中
fragment.appendChild(ball)
}
})
// 执行所有任务,将小球添加到 fragment 中
for (let i = 0; i < tasks.length; i++) {
tasks[i]()
}
// 将 fragment 添加到 ballBox 中
ballBox.appendChild(fragment)
}
但是效果依然不是很理想
使用requestIdleCallback利用空闲时间渲染
requestIdleCallback
是一个浏览器 API,它允许你在浏览器空闲时执行一些低优先级的任务。我们可以使用它来优化 addBall
函数中的任务调度,以避免阻塞主线程。
另外结合DocumentFragment
减少重绘共同作用优化渲染问题,下面的优化后的代码
function addBall() {
// 获取要添加小球的容器元素
let ballBox = document.getElementById('ballBox')
// 创建一个 DocumentFragment,用于批量添加小球
const fragment = document.createDocumentFragment()
// 创建一个包含 10000 个任务的数组,每个任务用于创建一个小球并添加到 fragment 中
const tasks = Array.from({ length: 10000 }, (_, i) => {
return () => {
// 创建一个 div 元素表示小球
let ball = document.createElement('div')
// 设置小球的文本内容为其序号
ball.innerText = i + 1
// 将小球添加到 fragment 中
fragment.appendChild(ball)
}
})
// 定义一个函数用于执行任务
function performTasks(tasks) {
// 如果任务数组为空,则将 fragment 添加到 ballBox 中
if (tasks.length === 0) {
ballBox.appendChild(fragment)
return
}
// 使用 requestIdleCallback 在浏览器空闲时执行任务
requestIdleCallback((deadline) => {
// 当浏览器有空闲时间且任务数组不为空时,执行任务
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
// 从任务数组中取出一个任务并执行
const task = tasks.shift()
task()
}
// 递归调用 performTasks 以继续执行剩余的任务
performTasks(tasks)
})
}
// 开始执行任务
performTasks(tasks)
}
deadline.timeRemaining()
是 requestIdleCallback
回调函数的参数 deadline
提供的方法之一。它返回当前空闲周期中剩余的毫秒数。这个方法允许你检查在当前空闲周期中还剩下多少时间可以用来执行任务
deadline.timeRemaining()
大于0表示还有空闲时间,利用这个时间来渲染小球,从而不会阻塞主线程,从而保持页面的响应性。
优化后的效果
可以看到,小球轻微的卡顿一下就接着继续运行