如何在浏览器中执行 100 万个任务而不卡顿?

35 阅读4分钟

在现代 Web 开发中,我们经常需要处理大量数据或执行高频率的任务。然而,如果一次性执行数十万甚至上百万个任务,很可能会导致页面“假死”或完全无响应。这是因为 JavaScript 是单线程语言,所有任务都在主线程上运行。

本文将详细介绍如何在浏览器中高效地执行 100 万个任务,同时保证页面流畅不卡顿。我们将使用多种技术手段来优化性能,包括分批执行、异步调度、Web Worker 多线程等,并结合 requestIdleCallback 提供的 deadline.timeRemaining() 方法进行任务调度控制。


🧠 一、理解问题本质

JavaScript 的执行环境是单线程的,这意味着:

  • 所有代码(包括 DOM 操作、事件回调、脚本逻辑)都运行在主线程上。
  • 如果你一次性执行了 100 万个同步任务(如一个大 for 循环),整个页面会被阻塞。
  • 页面无法渲染、用户无法交互,直到所有任务完成。

✅ 解决目标:

  • 避免阻塞主线程
  • 合理调度任务
  • 支持并发控制
  • 保持良好的用户体验

🔧 二、解决方案概览

技术作用
setTimeout / setImmediate分片执行任务,释放主线程
requestIdleCallback利用浏览器空闲时间执行任务
Web Worker将 CPU 密集型任务移出主线程
异步 Promise 调度器控制任务并发数量
MessageChannel主线程与 Worker 安全通信
进度条/虚拟滚动用户感知优化

🧩 三、具体实现方式

✅ 方法 1:使用 setTimeout 分片执行任务(适合轻量级任务)

function runTasks(tasks, batchSize = 1000) {
  let index = 0;

  function processBatch() {
    const end = Math.min(index + batchSize, tasks.length);
    for (; index < end; index++) {
      tasks[index](); // 执行任务
    }

    if (index < tasks.length) {
      setTimeout(processBatch, 0); // 下一批
    } else {
      console.log("所有任务完成");
    }
  }

  processBatch();
}

示例调用:

const tasks = [];
for (let i = 0; i < 1_000_000; i++) {
  tasks.push(() => {
    // 做一些轻量操作,如数组运算、状态变更等
    Math.sqrt(Math.random());
  });
}

runTasks(tasks);

✅ 方法 2:使用 requestIdleCallback(推荐用于 UI 优先的场景)

requestIdleCallback 是浏览器提供的 API,允许你在浏览器主线程空闲时执行任务。

使用示例:

function runTasksWithIdleCallback(tasks) {
  let index = 0;

  function performWork(deadline) {
    while (deadline.timeRemaining() > 0 && index < tasks.length) {
      tasks[index]();
      index++;
    }

    if (index < tasks.length) {
      requestIdleCallback(performWork);
    } else {
      console.log("所有任务已完成");
    }
  }

  requestIdleCallback(performWork);
}

⚠️ 注意:requestIdleCallback 是实验性 API,在 Firefox 和 Safari 中尚未支持。但如果你的目标平台是 Chrome 或 Edge,这是一个非常强大的工具。


✅ 方法 3:使用 Web Worker 处理 CPU 密集型任务

对于计算密集型任务(如图像处理、加密解密、大规模数据排序等),建议使用 Web Worker 将任务移出主线程。

worker.js

onmessage = function(e) {
  const { tasks } = e.data;
  for (let task of tasks) {
    // 执行任务逻辑
  }
  postMessage("done");
};

main.js

const worker = new Worker('worker.js');

// 拆分任务为多个批次发送给 Worker
const chunkSize = 10000;
for (let i = 0; i < 100; i++) {
  const start = i * chunkSize;
  const end = start + chunkSize;
  const subTasks = tasks.slice(start, end);

  worker.postMessage({ tasks: subTasks });
}

worker.onmessage = function(e) {
  console.log("Worker 返回:", e.data);
};

📈 四、高级技巧:任务调度器 + 并发控制

你可以构建一个简单的任务调度器来控制并发数和执行顺序。

async function runConcurrentTasks(tasks, concurrency = 5) {
  const queue = [...tasks];
  const running = [];
  
  while (queue.length || running.length) {
    while (running.length < concurrency && queue.length) {
      const task = queue.shift();
      const p = task().finally(() => {
        running.splice(running.indexOf(p), 1);
      });
      running.push(p);
    }
    await Promise.race(running);
  }
}

🧭 五、可视化与反馈(可选)

  • 使用 <progress> 元素显示进度
  • 在每批任务完成后更新 DOM(注意:不要频繁更新)
  • 使用 postMessage 向主线程汇报进度

✅ 六、deadline.timeRemaining() 的使用详解

deadline.timeRemaining()requestIdleCallback 提供的方法,返回当前帧剩余的可用时间(单位:毫秒)。

示例:

requestIdleCallback((deadline) => {
  console.log('剩余可用时间:', deadline.timeRemaining(), 'ms');
});

属性说明:

属性类型说明
timeRemaining()function返回当前帧剩余的空闲时间(单位:毫秒)
didTimeoutboolean如果设置了超时时间并已超时,则为 true

使用方法:

function processTasks(deadline) {
  let count = 0;

  while (deadline.timeRemaining() > 1 && tasks.length > 0) {
    const task = tasks.pop();
    task();
    count++;
  }

  console.log(`本次执行了 ${count} 个任务`);

  if (tasks.length > 0) {
    requestIdleCallback(processTasks);
  } else {
    console.log("所有任务已完成");
  }
}

requestIdleCallback(processTasks);

可选参数:设置超时时间

requestIdleCallback((deadline) => {
  console.log('是否超时:', deadline.didTimeout);
  if (deadline.didTimeout || deadline.timeRemaining() < 1) {
    // 超时或时间不足,延迟执行
    setTimeout(() => requestIdleCallback(processTasks), 0);
  } else {
    // 继续执行任务
    processTasks(deadline);
  }
}, { timeout: 2000 }); // 最多等待2秒后强制执行

🧪 七、注意事项

项目建议
单个任务耗时应尽量短,<10ms
避免频繁 DOM 操作使用虚拟列表、节流、防抖
内存占用100万个任务对象可能会占用大量内存,建议按需生成
错误处理使用 try/catch 包裹任务函数,防止崩溃
任务优先级如果有高优先级任务,可以中断低优先级任务

🎯 总结:如何选择合适方案?

场景推荐方案
简单任务,少量计算setTimeout 分批执行
用户体验优先requestIdleCallback
CPU 密集型任务Web Worker
需要并发控制异步 Promise 调度器
需要长期后台运行Service Worker + IndexedDB 缓存任务

💡 结语

通过上述方法,我们可以有效地在浏览器中处理 100 万个任务,而不会造成页面卡顿。关键在于:

  • 利用浏览器空闲时间
  • 分片执行任务
  • 使用 Web Worker 避免阻塞主线程
  • 合理控制并发数量

📌 关键词requestIdleCallback, timeRemaining, Web Worker, setTimeout, 异步任务调度, 浏览器性能优化, 100万任务处理