背景
在一次性需要处理大量dom渲染操作时,直接渲染页面会非常耗费性能,从而阻塞js渲染进程,造成页面卡顿,比如在页面上一次性插入50万个dom操作,代码如下:
js
for (let i = 0; i < 500000; i++) {
const div = document.createElement('div');
div.innerText = i;
document.body.appendChild(div);
}
优化
在某些情况下,我们可以使用createDocumentFragment
虚拟节点来减少频繁的dom操作
js
const fragment = document.createDocumentFragment();
for (let i = 0; i < 5000; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
document.body.appendChild(fragment);
这段代码大大减少了频繁操作dom,只在最后一次性将dom挂载到页面,但最终一次性挂载50万个节点的dom还是会非常耗费性能,页面依然卡顿,但不否认它确实有一定的优化效果,只是在这里优化效果并不明显,所以我们需要将任务进行拆分,进行分批渲染,从而达到优化的目的
如上所述,首先创建一个任务列表tasks
,然后在合适的时间去分批次执行它,这里直接使用Array.from
直接创建性能会更好
这里我们选择使用requestIdleCallback
,它的作用是在每一帧渲染后如果还有空余时间就会触发,当然如果没有空余时间就不会触发
js
// 任务列表
const tasks = Array.from({ length: 500000 }, (_, i) => () => {
const div = document.createElement('div');
div.innerText = i;
document.body.appendChild(div);
});
function performTask(tasks) {
let index = 0;
function _run() {
requestIdleCallback((deadline) => {
// 在任务没执行完并且空余时间大于0的情况下执行任务
while (index < tasks.length && deadline.timeRemaining() > 0) {
tasks[index++]();
}
if (index < tasks.length) {
_run();
}
});
}
_run();
}
performTask(tasks);
这样就会在渲染空余时间执行一部分任务,直到任务执行完毕,不会阻塞渲染进程
到这里,代码其实已经优化完成,接下来写的是如何对这个函数进行封装,让它变得更通用
封装
要封装通用型函数,我们首先要去掉一些约束项,也就是每次的执行时机和每次执行的量,这里把requestIdleCallback
替换成了需要参数传递的sheduler
调度器函数,它也接收一个函数作为参数(中间的while if
部分)
js
function performTask(tasks, sheduler) {
let index = 0;
function _run() {
sheduler((isGoOn) => {
while (index < tasks.length && isGoOn()) {
tasks[index++]();
}
if (index < tasks.length) {
_run();
}
});
}
_run();
}
这里的runChunk
执行的实际上就是while if
那段代码:
js
(isGoOn) => {
while (index < tasks.length && isGoOn()) {
tasks[index++]();
}
if (index < tasks.length) {
_run();
}
}
runChunk
又接收一个函数来判断每次执行的量isGoOn
,这里看起来很乱,来回嵌套,但是却非常的灵活,比如可以自定义一个调度器,让它每隔一秒钟执行三个任务
js
function performTask(tasks, sheduler) {
let index = 0;
function _run() {
sheduler((isGoOn) => {
while (index < tasks.length && isGoOn()) {
tasks[index++]();
}
if (index < tasks.length) {
_run();
}
});
}
_run();
}
const tasks = Array.from({ length: 500000 }, (_, i) => () => {
const div = document.createElement('div');
div.innerText = i;
document.body.appendChild(div);
});
// 每隔1秒执行三个任务
const sheduler = (runChunk) => {
let count = 0;
setTimeout(() => {
// runChunk(() => true); // 如果isGoOn为true,则会一直执行到任务完毕
runChunk(() => count++ < 3); // 每次执行3个任务
}, 1000);
// 上面方便阅读,这里可以简写
// setTimeout(() => runChunk(() => count++ < 3), 1000);
}
performTask(tasks, sheduler);
这还没完,你还可以使用requestAnimationFrame
作为调度器来使用它,requestAnimationFrame
这个方法是会在下一次渲染之前触发,一般用于动画渲染,在这里也可以用它来优化dom渲染
js
// 其他代码如上...
const sheduler = (runChunk) => {
let count = 0;
requestAnimationFrame(() => {
runChunk(() => count++ < 3);
});
}
performTask(tasks, sheduler);
当然,通常情况下,我们使用requestIdleCallback
情况相对会比较多,所以为了方便使用,我们可以再针对这个封装一个便携性的函数
js
// 其他代码如上...
function idlePerformTask(tasks) {
performTask(tasks, (runChunk) => {
requestIdleCallback((deadline) => {
runChunk(() => deadline.timeRemaining() > 0);
});
});
}
优化后的完整代码如下:
js
function performTask(tasks, sheduler) {
let index = 0;
function _run() {
sheduler((isGoOn) => {
while (index < tasks.length && isGoOn()) {
tasks[index++]();
}
if (index < tasks.length) {
_run();
}
});
}
_run();
}
function idlePerformTask(tasks) {
performTask(tasks, (runChunk) => {
requestIdleCallback((deadline) => {
runChunk(() => deadline.timeRemaining() > 0);
});
});
}
const tasks = Array.from({ length: 500000 }, (_, i) => () => {
const div = document.createElement('div');
div.innerText = i;
document.body.appendChild(div);
});
idlePerformTask(tasks);
总结
本文主要总结了前端大量渲染dom导致的页面卡顿的优化实践和分时函数的封装方法,要知道requestIdleCallback
和requestAnimationFrame
的执行时机,也顺带提到了createDocumentFragment
虚拟节点在开发中的应用