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

背景

在一次性需要处理大量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虚拟节点在开发中的应用

相关推荐
kymjs张涛6 分钟前
零一开源|前沿技术周刊 #10
java·前端·面试
Sawtone12 分钟前
[前端] Leader:可以不用但要知道😠一文速查 TypeScript 基础知识点,字典式速查,全文干货!
前端
Wcowin13 分钟前
mkdocs-document-dates
前端·github
用户102207917571119 分钟前
表格拖拽原生实现
前端·javascript
五月君_20 分钟前
见证历史:Vite 首次超越 Webpack!
前端·webpack·node.js
小old弟20 分钟前
前端开发,Promise 从原理到实现,一文通
前端
nickzone21 分钟前
Next.js + Shopify OAuth 第三方应用接入完整指南
前端
xyphf_和派孔明24 分钟前
web前端React和Vue框架与库安全实践
前端·javascript·前端框架
一只小风华~34 分钟前
JavaScript:Ajax(异步通信技术)
前端·javascript·ajax·web
一个很帅的帅哥1 小时前
JavaScriptAJAX异步请求:XHR、Fetch与Axios对比
javascript·axios·xmlhttprequest·fetch