Scheduler API 实战:浏览器任务优先级调度与主线程优化
主线程调度的困境
浏览器主线程同时承担 UI 渲染、事件处理、JavaScript 执行。长任务(>50ms)会阻塞交互,导致 INP 指标劣化:
| 传统方案 | 问题 |
|---|---|
| setTimeout(fn, 0) | 实际延迟 4ms,无法控制优先级 |
| requestIdleCallback | 只在空闲时执行,无法保证时效 |
| await scheduler.yield() | ✅ 精确让出,保持调度顺序 |
| scheduler.postTask() | ✅ 优先级 + 让出 + 取消,完整调度 |
Scheduler API 优先级模型
scheduler.postTask() 提供三级优先级,映射到浏览器内部调度:
| 优先级 | 含义 | 典型用途 | 对应 Chrome 内部 |
|---|---|---|---|
user-blocking |
最高,立即执行 | 用户输入响应、动画 | Very High |
user-visible |
中等,尽快执行 | DOM 更新、数据解析 | High / Medium |
background |
最低,空闲执行 | 日志上报、预计算 | Low / Best Effort |
基本用法
const result = await scheduler.postTask(
() => heavyComputation(data),
{ priority: 'background' }
);
实战:图片压缩任务的优先级调度
工具库 图片压缩 处理批量图片时,需要同时响应用户操作:
async function handleBatchCompress(files: File[]) {
const controller = new TaskController({ priority: 'background' });
for (const file of files) {
// 低优先级:压缩计算不阻塞 UI
scheduler.postTask(
async () => {
const result = await compressImage(file, { quality: 0.85 });
// 中优先级:更新 DOM 让用户看到进度
await scheduler.postTask(
() => updateProgressUI(file.name, result),
{ priority: 'user-visible' }
);
},
{ signal: controller.signal }
);
}
return controller;
}
用户点击"取消"时,controller.abort() 立即停止所有待执行任务。
scheduler.yield():精确让出主线程
scheduler.yield() 比 setTimeout(fn, 0) 更精确——它让出主线程给更高优先级任务,且保持当前任务的执行顺序:
async function processLargeDataset(items: Item[]) {
const results: Result[] = [];
for (let i = 0; i < items.length; i++) {
results.push(transform(items[i]));
// 每 50 项让出一次,避免长任务
if (i % 50 === 0) {
await scheduler.yield();
}
}
return results;
}
yield() vs setTimeout() 对比
| 特性 | setTimeout(fn, 0) | scheduler.yield() |
|---|---|---|
| 最小延迟 | 4ms | 0ms |
| 优先级感知 | ❌ 无 | ✅ 让出给更高优先级 |
| 执行顺序 | 不保证 | ✅ 保持调度顺序 |
| 长任务拆分 | ✅ | ✅ |
| 浏览器支持 | 全部 | Chrome 115+ |
TaskController:取消与动态优先级
const controller = new TaskController({ priority: 'user-visible' });
// 提交任务
scheduler.postTask(() => renderChart(data), { signal: controller.signal });
// 动态降级:用户切换标签页时降低优先级
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
controller.setPriority('background');
} else {
controller.setPriority('user-visible');
}
});
// 取消所有任务
controller.abort();
避免 Long Task 的策略组合
策略 1:分片 + yield
async function chunkedProcess<T>(items: T[], fn: (item: T) => void, chunkSize = 20) {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
chunk.forEach(fn);
await scheduler.yield();
}
}
策略 2:优先级分层
function scheduleWork(work: WorkItem) {
const priority = work.type === 'user-input' ? 'user-blocking'
: work.type === 'render' ? 'user-visible'
: 'background';
return scheduler.postTask(work.execute, { priority });
}
策略 3:AbortController 集成
const ac = new AbortController();
scheduler.postTask(() => mergePDFs(files), {
priority: 'background',
signal: ac.signal
});
// 用户导航离开时取消
window.addEventListener('beforeunload', () => ac.abort());
性能实测:PDF 合并场景
对 PDF 合并 处理 20 个 PDF 文件的实测:
| 方案 | 总耗时 | INP (ms) | 主线程阻塞 |
|---|---|---|---|
| 同步循环 | 3.2s | 480 | 3.2s |
| setTimeout 分片 | 3.3s | 120 | 0.4s |
| postTask + yield | 3.2s | 60 | 0.1s |
总耗时不变,但 INP 从 480ms 降至 60ms,交互响应提升 8 倍。
兼容性与降级
async function yieldToMain() {
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
return new Promise(resolve => setTimeout(resolve, 0));
}
}
function postTask(fn: () => void, options?: { priority?: string }) {
if ('scheduler' in window) {
return scheduler.postTask(fn, options as any);
}
return Promise.resolve(fn());
}
常见问题
postTask 和 requestIdleCallback 怎么选?
requestIdleCallback 只在浏览器空闲时执行,适合完全不紧急的工作(如预加载)。postTask 的 background 优先级类似但更可控,且支持取消和动态优先级调整。
yield() 会增加总耗时吗?
单次 yield() 开销约 0.1ms。每 50 项 yield 一次处理 10000 项,额外开销仅 20ms,远小于避免的阻塞时间。
如何调试优先级?
Chrome DevTools → Performance 面板中,任务标注了优先级信息。也可用 scheduler.postTask(() => console.trace(), { priority: 'background' }) 追踪调度。
总结
Scheduler API 为浏览器主线程提供了精确的优先级调度能力。scheduler.postTask() 实现三级优先级任务分发,scheduler.yield() 精确让出主线程避免长任务,TaskController 支持取消与动态优先级调整。这是构建高性能、高响应 Web 应用的关键基础设施。
本站提供浏览器本地工具,免注册即可试用 →