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