前端大数据渲染性能优化 - 分时函数的封装

背景

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

相关推荐
油丶酸萝卜别吃15 分钟前
jquery-ajax.js 说明文档
前端·javascript·jquery
windliang22 分钟前
Claude Code 源码分析(九):子 Agent 如何分叉、继续与回到父会话
前端·javascript·面试
柒和远方1 小时前
V063: TS 面试必考:interface 与 type 的四大差异,与 LLM Harness 的自动化择优
前端·javascript
半个落月1 小时前
React useRef 详解:DOM 引用、持久化值与 Worker 实例
前端·react.js
阿黎梨梨1 小时前
TypeScript 类型编程:从新手到 Harness 工程实践
前端
黄金决明子1 小时前
Vue3 + Vite 打包后打开空白?
前端
张龙6871 小时前
终端效率翻倍实战:fzf + zoxide + ripgrep + bat 组合拳,告别重复敲命令
前端
宿6741 小时前
vue3-env环境
前端·vue.js
蔬菜_1 小时前
前端转全栈-day5(数组、list、set)
java·前端·数据结构·list
汉堡大王95271 小时前
Vue 3 + TS + Element Plus 实战:如何从零搭建企业级违章记录管理 SaaS 前端
前端·javascript·vue.js