在前端开发中,处理耗时较长的任务(Long Tasks)是性能优化的核心难点。如果主线程被占用超过 50ms,用户就会感觉到页面卡顿(掉帧)。
为了解决这个问题,深入探讨几种核心技术方案,并编写一套较为完整的、接近生产环境的任务调度与优化系统。这将包含以下几个模块:
- 基于生成器(Generator)的时间分片调度器:将同步大任务拆解为异步小任务。
- 多线程并行计算框架(Web Worker Pool):利用多核 CPU 处理纯计算任务。
- 高优先级抢占式调度模拟(类似于 React Scheduler):通过 MessageChannel 实现宏任务调度。
- 大量数据渲染优化(虚拟列表核心实现):解决渲染层面的长任务。
第一部分:通用时间分片调度器 (Time Slicing Scheduler)
这是解决主线程阻塞最直接的方法。我们将利用 ES6 的 Generator 函数特性,配合 requestAnimationFrame 或 MessageChannel,将一个巨大的循环拆分成多个可以在每一帧之间暂停执行的小块。
javascript
/**
* 模块一:TimeSlicer.js
* 描述:一个基于生成器函数的通用时间分片控制器。
* 它可以暂停、恢复任务,并确保每帧执行时间不超过阈值(如 16ms)。
*/
class TimeSlicer {
constructor(options = {}) {
// 默认一帧的时间预算,留给浏览器渲染的时间
this.frameBudget = options.frameBudget || 12; // 12ms 给 JS,4ms 给渲染
this.fps = options.fps || 60;
this.isRunning = false;
this.queue = []; // 任务队列
// 绑定上下文
this._performWork = this._performWork.bind(this);
}
/**
* 添加一个需要分片执行的任务
* @param {Generator Function} generatorFunc - 生成器函数
* @param {Object} context - 执行上下文
* @param {...any} args - 参数
*/
addTask(generatorFunc, context = null, ...args) {
if (typeof generatorFunc !== 'function' || generatorFunc.constructor.name !== 'GeneratorFunction') {
throw new Error('TimeSlicer: 任务必须是一个 Generator 函数');
}
const task = {
iterator: generatorFunc.apply(context, args),
priority: 0, // 预留优先级字段
createdTime: Date.now()
};
this.queue.push(task);
this._schedule();
}
/**
* 调度核心
* 如果当前没有运行,则启动调度
*/
_schedule() {
if (!this.isRunning && this.queue.length > 0) {
this.isRunning = true;
// 优先使用 MessageChannel (宏任务),其次 requestAnimationFrame
if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
channel.port2.onmessage = this._performWork;
channel.port1.postMessage(null);
} else {
setTimeout(this._performWork, 0);
}
}
}
/**
* 执行工作单元
* 在给定的时间预算内尽可能多地执行 generator.next()
*/
_performWork() {
const startTime = performance.now();
// 只要队列里有任务,且当前帧还有剩余时间,就继续执行
while (this.queue.length > 0 && (performance.now() - startTime < this.frameBudget)) {
const currentTask = this.queue[0];
try {
// 执行一步
const result = currentTask.iterator.next();
// 如果当前任务完成 (done: true)
if (result.done) {
this.queue.shift(); // 移除已完成任务
// 可以在这里触发任务完成的回调
console.log(`[TimeSlicer] 任务完成,耗时: ${(Date.now() - currentTask.createdTime)}ms`);
}
// 如果没完成,它会保留在队列头部,下一次循环继续执行
} catch (error) {
console.error('[TimeSlicer] 任务执行出错:', error);
this.queue.shift(); // 出错移除任务,防止死循环
}
}
// 检查是否还有剩余任务
if (this.queue.length > 0) {
// 让出主线程,等待下一次调度
// 使用 requestAnimationFrame 配合 MessageChannel 达到最佳效果
// 这里简化逻辑,直接重新调度
if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
channel.port2.onmessage = this._performWork;
channel.port1.postMessage(null);
} else {
setTimeout(this._performWork, 0);
}
} else {
this.isRunning = false;
}
}
}
// ==========================================
// 使用示例代码:处理 10万条数据的复杂计算
// ==========================================
const slicer = new TimeSlicer();
/**
* 模拟一个耗时的大型计算任务
* 假设我们需要处理一个巨大的数组,每项都要进行数学运算
*/
function* heavyCalculationTask(dataSize) {
const results = [];
console.log(`[Task] 开始处理 ${dataSize} 条数据...`);
for (let i = 0; i < dataSize; i++) {
// 模拟复杂计算: 三角函数、开方等
let temp = Math.sqrt(i) * Math.tan(i) + Math.random();
// 模拟 DOM 操作(如果需要,虽然不建议在逻辑中混杂 DOM)
// 仅仅是纯计算
results.push(temp);
// 关键点:每处理 1000 条数据,或者每一步 yield 一次
// 粒度越细,响应越好,但总耗时会微增
if (i % 500 === 0) {
yield; // 暂停,交还控制权给调度器
}
}
console.log(`[Task] 所有数据处理完毕,结果长度: ${results.length}`);
return results;
}
// 启动任务:处理 100,000 条数据
// 如果不使用 TimeSlicer,这行代码会直接卡死浏览器 2-3秒
slicer.addTask(heavyCalculationTask, null, 100000);
// 可以同时添加另一个任务,它们会交替执行
slicer.addTask(function* () {
for(let i=0; i<50; i++) {
console.log(`[Task 2] 穿插执行中... ${i}`);
yield;
}
});
第二部分:Web Worker 线程池 (Thread Pool)
时间分片虽然解决了卡顿,但任务仍然在主线程运行,总耗时并没有减少(甚至因为调度开销变长了)。对于纯计算任务(如图像处理、大文件解析、加密解密),最好的方案是移出主线程。
为了避免频繁创建 Worker 带来的开销,我们需要实现一个 Worker 线程池。
javascript
/**
* 模块二:WorkerPool.js
* 描述:管理一组 Web Worker,实现负载均衡和任务复用。
*/
class WorkerPool {
constructor(workerScript, poolSize = 4) {
this.workerScript = workerScript;
this.poolSize = poolSize || navigator.hardwareConcurrency || 4;
this.workers = []; // 存储 { worker, isBusy, id }
this.queue = []; // 等待处理的任务队列
this.taskMap = new Map(); // 存储 taskId -> { resolve, reject }
this._initPool();
}
_initPool() {
for (let i = 0; i < this.poolSize; i++) {
const worker = new Worker(this.workerScript);
worker.onmessage = (e) => this._handleWorkerMessage(e, i);
worker.onerror = (e) => this._handleWorkerError(e, i);
this.workers.push({
id: i,
worker: worker,
isBusy: false
});
}
console.log(`[WorkerPool] 初始化完成,包含 ${this.poolSize} 个线程`);
}
/**
* 提交任务到线程池
* @param {string} type - 任务类型
* @param {any} data - 数据
* @returns {Promise}
*/
run(type, data) {
return new Promise((resolve, reject) => {
const taskId = this._generateUUID();
const task = {
taskId,
type,
data,
resolve,
reject
};
// 尝试寻找空闲 Worker
const idleWorkerIndex = this.workers.findIndex(w => !w.isBusy);
if (idleWorkerIndex !== -1) {
this._dispatchTask(idleWorkerIndex, task);
} else {
// 所有 Worker 都在忙,加入队列
this.queue.push(task);
}
});
}
_dispatchTask(workerIndex, task) {
const workerObj = this.workers[workerIndex];
workerObj.isBusy = true;
// 记录任务回调,以便收到消息时触发
this.taskMap.set(task.taskId, {
resolve: task.resolve,
reject: task.reject
});
// 发送给 Worker
workerObj.worker.postMessage({
taskId: task.taskId,
type: task.type,
data: task.data
});
}
_handleWorkerMessage(e, workerIndex) {
const { taskId, result, error } = e.data;
const workerObj = this.workers[workerIndex];
// 标记为空闲
workerObj.isBusy = false;
// 找到对应的 Promise
if (this.taskMap.has(taskId)) {
const { resolve, reject } = this.taskMap.get(taskId);
if (error) {
reject(error);
} else {
resolve(result);
}
this.taskMap.delete(taskId);
}
// 检查队列中是否有等待的任务
if (this.queue.length > 0) {
const nextTask = this.queue.shift();
this._dispatchTask(workerIndex, nextTask);
}
}
_handleWorkerError(e, workerIndex) {
console.error(`[WorkerPool] Worker ${workerIndex} 发生底层错误`, e);
// 实际生产中可能需要重启该 Worker
this.workers[workerIndex].isBusy = false;
}
_generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
terminate() {
this.workers.forEach(w => w.worker.terminate());
this.workers = [];
this.queue = [];
this.taskMap.clear();
}
}
/*
--------------------------------------------------
配套的 Worker 脚本内容 (worker-script.js)
(在实际项目中,这通常是一个单独的文件,或者通过 Blob URL 生成)
--------------------------------------------------
*/
/*
// worker-script.js 伪代码实现
self.onmessage = function(e) {
const { taskId, type, data } = e.data;
try {
let result;
switch (type) {
case 'SORT_LARGE_ARRAY':
// 模拟耗时排序
result = data.sort((a, b) => a - b);
break;
case 'IMAGE_FILTER':
// 模拟图像像素处理
result = applyFilter(data);
break;
default:
throw new Error('Unknown task type');
}
self.postMessage({ taskId, result });
} catch (err) {
self.postMessage({ taskId, error: err.message });
}
};
function applyFilter(pixels) {
// 模拟 O(n) 遍历
let sum = 0;
for(let i=0; i<10000000; i++) { sum += i }
return pixels;
}
*/
// 使用示例
// const pool = new WorkerPool('worker-script.js');
// pool.run('SORT_LARGE_ARRAY', [5, 1, 9, ...]).then(res => console.log(res));
第三部分:任务优先级与空闲调度 (Idle & Priority Scheduler)
除了时间分片,我们还可以利用 requestIdleCallback 在浏览器空闲时执行低优先级任务(如埋点上报、预加载数据)。为了兼容性和更好的控制,我们实现一个带优先级的任务队列。
javascript
/**
* 模块三:PriorityScheduler.js
* 描述:模拟 React Scheduler,支持优先级(UserBlocking, Normal, Low, Idle)。
*/
const Priority = {
Immediate: 1, // 点击事件等,必须立即执行
UserBlocking: 2, // 滚动、输入,需要在短时间内完成
Normal: 3, // 普通数据请求
Low: 4, // 动画后续处理
Idle: 5 // 埋点、预加载
};
class PriorityScheduler {
constructor() {
this.taskQueue = [];
this.isMessageLoopRunning = false;
// 使用 MessageChannel 进行任务循环 tick
const channel = new MessageChannel();
this.port = channel.port2;
channel.port1.onmessage = this._performWorkUntilDeadline.bind(this);
}
/**
* 调度任务
* @param {Function} callback
* @param {number} priorityLevel
*/
scheduleCallback(priorityLevel, callback) {
const startTime = performance.now();
let timeout;
// 根据优先级设置过期时间
switch (priorityLevel) {
case Priority.Immediate: timeout = -1; break;
case Priority.UserBlocking: timeout = 250; break;
case Priority.Normal: timeout = 5000; break;
case Priority.Low: timeout = 10000; break;
case Priority.Idle: timeout = 1073741823; break; // Max Int 32 bit approx
default: timeout = 5000;
}
const expirationTime = startTime + timeout;
const newTask = {
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1 // 用于最小堆排序,这里简化为数组排序
};
// 插入队列并按过期时间排序(模拟最小堆)
this.taskQueue.push(newTask);
this.taskQueue.sort((a, b) => a.expirationTime - b.expirationTime);
if (!this.isMessageLoopRunning) {
this.isMessageLoopRunning = true;
this.port.postMessage(null);
}
}
_performWorkUntilDeadline() {
const currentTime = performance.now();
const deadline = currentTime + 5; // 每次给 5ms 的时间片
let currentTask = this.taskQueue[0];
while (currentTask) {
// 如果任务还没过期,但当前帧时间片用完了,暂停,让给浏览器渲染
if (currentTask.expirationTime > currentTime && performance.now() >= deadline) {
break;
}
// 执行任务
const callback = currentTask.callback;
// 可以在这里传入 didTimeout 参数告知任务是否超时
const didTimeout = currentTask.expirationTime <= currentTime;
// 从队列移除
this.taskQueue.shift();
try {
// 执行回调
const continuationCallback = callback(didTimeout);
// 如果任务返回了一个函数,说明它还没做完(支持任务中断与恢复)
if (typeof continuationCallback === 'function') {
// 重新加入队列,保持原有的过期时间
currentTask.callback = continuationCallback;
this.taskQueue.push(currentTask);
this.taskQueue.sort((a, b) => a.expirationTime - b.expirationTime);
}
} catch (e) {
console.error('Task error:', e);
}
currentTask = this.taskQueue[0];
}
if (this.taskQueue.length > 0) {
// 还有任务,继续请求下一个 tick
this.port.postMessage(null);
} else {
this.isMessageLoopRunning = false;
}
}
}
// 使用示例
const scheduler = new PriorityScheduler();
// 低优先级任务:发送统计数据
scheduler.scheduleCallback(Priority.Idle, (didTimeout) => {
console.log('Idle task executing, didTimeout:', didTimeout);
// 模拟重负载
const start = Date.now();
while(Date.now() - start < 10) {}
});
// 高优先级任务:响应用户点击
scheduler.scheduleCallback(Priority.Immediate, () => {
console.log('Immediate task executing!');
});
第四部分:虚拟列表 (Virtual List) - 解决渲染长任务
当数据量达到几万条时,最大的"长任务"通常不是 JS 计算,而是 DOM 的 Layout 和 Paint。虚拟列表技术只渲染可视区域内的元素,极大减少 DOM 节点数量。
为了保证代码量和细节,我们不使用 React/Vue,而是用原生 JS 实现一个高效的虚拟滚动类。
javascript
/**
* 模块四:VirtualScroller.js
* 描述:原生高性能虚拟滚动实现。
* 支持动态高度估算(简化版),DOM 复用,和滚动节流。
*/
class VirtualScroller {
/**
* @param {HTMLElement} container - 滚动容器
* @param {Array} items - 数据源
* @param {Function} rowRenderer - 行渲染函数 (index, data) => HTMLElement
* @param {number} itemHeight - 固定行高
*/
constructor(container, items, rowRenderer, itemHeight = 50) {
this.container = container;
this.items = items;
this.rowRenderer = rowRenderer;
this.itemHeight = itemHeight;
// 内部状态
this.visibleCount = 0;
this.startIndex = 0;
this.endIndex = 0;
this.scrollTop = 0;
// 缓冲区域(上下多渲染几个,防止白屏)
this.buffer = 5;
this._initDOM();
this._bindEvents();
this._update();
}
_initDOM() {
this.container.style.overflowY = 'auto';
this.container.style.position = 'relative';
// 创建占位高度层 (Phantom)
// 它的高度等于 总数据量 * 行高,用于撑开滚动条
this.phantomContent = document.createElement('div');
this.phantomContent.style.position = 'absolute';
this.phantomContent.style.left = '0';
this.phantomContent.style.top = '0';
this.phantomContent.style.right = '0';
this.phantomContent.style.zIndex = '-1';
this.phantomContent.style.height = `${this.items.length * this.itemHeight}px`;
// 创建实际内容层 (Real Content)
// 这里的元素会绝对定位,或者通过 transform 偏移
this.content = document.createElement('div');
this.content.style.position = 'absolute';
this.content.style.left = '0';
this.content.style.right = '0';
this.content.style.top = '0';
this.container.appendChild(this.phantomContent);
this.container.appendChild(this.content);
}
_bindEvents() {
// 简单的节流处理
let ticking = false;
this.container.addEventListener('scroll', (e) => {
if (!ticking) {
window.requestAnimationFrame(() => {
this.scrollTop = e.target.scrollTop;
this._update();
ticking = false;
});
ticking = true;
}
});
}
_update() {
// 1. 计算可视区域能放下多少个元素
const containerHeight = this.container.clientHeight;
this.visibleCount = Math.ceil(containerHeight / this.itemHeight);
// 2. 计算起始索引
// 向下取整,确保滚动平滑
this.startIndex = Math.floor(this.scrollTop / this.itemHeight);
// 3. 考虑缓冲区
const renderStart = Math.max(0, this.startIndex - this.buffer);
// 4. 计算结束索引
const renderEnd = Math.min(
this.items.length,
this.startIndex + this.visibleCount + this.buffer
);
// 5. 渲染这一段数据
this._renderRows(renderStart, renderEnd);
// 6. 偏移 content 层,让它处于正确的位置
// 因为我们只渲染了一部分,所以需要把这部分内容移动到视觉上的位置
// 这里的偏移量应该是 renderStart * itemHeight
const offset = renderStart * this.itemHeight;
this.content.style.transform = `translateY(${offset}px)`;
}
_renderRows(start, end) {
// 简单的 Diff 算法:全清空再添加 (生产环境应复用 DOM)
// 为了演示代码量和逻辑,这里做一个简单的 DOM 复用池逻辑
const fragment = document.createDocumentFragment();
// 清空当前渲染层
this.content.innerHTML = '';
for (let i = start; i < end; i++) {
const itemData = this.items[i];
const node = this.rowRenderer(i, itemData);
// 设置必要的样式确保高度正确
node.style.height = `${this.itemHeight}px`;
node.style.boxSizing = 'border-box'; // 避免 padding 影响高度
fragment.appendChild(node);
}
this.content.appendChild(fragment);
}
/**
* 动态更新数据
*/
updateData(newItems) {
this.items = newItems;
this.phantomContent.style.height = `${this.items.length * this.itemHeight}px`;
this._update();
}
}
// ====================================
// VirtualScroller 使用测试
// ====================================
/*
// HTML 结构: <div id="scroll-container" style="height: 500px; width: 300px;"></div>
const data = Array.from({ length: 100000 }, (_, i) => ({ id: i, text: `Row Item ${i}` }));
const container = document.getElementById('scroll-container');
const scroller = new VirtualScroller(container, data, (index, item) => {
const div = document.createElement('div');
div.textContent = `${item.id} - ${item.text}`;
div.style.borderBottom = '1px solid #ccc';
div.style.display = 'flex';
div.style.alignItems = 'center';
div.style.paddingLeft = '10px';
// 添加一些复杂的 DOM 结构来模拟渲染压力
// 如果没有虚拟列表,10万个这样的 DOM 会瞬间卡死
const span = document.createElement('span');
span.innerText = ' [Detail]';
span.style.color = 'blue';
div.appendChild(span);
return div;
}, 40);
*/
总结与进阶思考
以上代码展示了解决前端"长任务"的四个维度的工程化实现。要真正掌握优化,需要理解以下核心思想:
- 让出主线程 (Yielding) : JS 是单线程的,必须通过
TimeSlicer(时间分片) 将大任务切碎,给 UI 渲染留出呼吸的时间。 - 并行计算 (Parallelism) : 使用
WorkerPool将不涉及 DOM 的纯算法逻辑移到另一个线程。 - 优先级调度 (Scheduling) : 并不是所有任务都一样重要。
PriorityScheduler确保用户交互(点击、输入)永远优先于数据处理。 - 按需渲染 (Lazy Rendering) :
VirtualScroller证明了无论数据多大,只要视口有限,DOM 的数量就应该保持恒定。
这些代码模块可以直接组合使用。例如,在一个大型数据分析看板中:
- 使用 WorkerPool 在后台拉取并解析 10MB 的 JSON 数据。
- 解析完成后,使用 TimeSlicer 对数据进行预处理(格式化、计算环比)。
- 最终展示时,使用 VirtualScroller 将十万条表格数据流畅地渲染出来。