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) {
scheduler.postTask(
async () => {
const result = await compressImage(file, { quality: 0.85 });
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]));
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 應用的關鍵基礎設施。
本站提供瀏覽器本地工具,免註冊即可試用 →