背景

在一次性需要处理大量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导致的页面卡顿的优化实践和分时函数的封装方法,要知道requestIdleCallbackrequestAnimationFrame的执行时机,也顺带提到了createDocumentFragment虚拟节点在开发中的应用

个人笔记记录 2021 ~ 2025