1988 字
10 分鐘

scheduler.postTask 怎麼用?JavaScript 任務優先級與 fallback

當頁面同時要處理輸入、更新畫面、分析資料與背景同步時,setTimeout() 只能表達「稍後執行」,不能直接表達「這件事比那件事更重要」。Prioritized Task Scheduling API 的 scheduler.postTask() 提供三個粗粒度優先級、Promise 結果、延遲與取消控制,適合把不同工作排進同一個瀏覽器排程模型。

直接答案是:需要區分互動工作與背景工作時使用 scheduler.postTask(),先用 globalThis.scheduler?.postTask 做 feature detection;不支援時退回 setTimeout() 或既有的分段策略,並記得優先級不會讓同步 callback 自動變快。 這個 API 目前不是 Baseline,不能直接當成所有瀏覽器都支援的基礎設施。

本文依 MDN:Scheduler.postTask()MDN:Prioritized Task Scheduling API 整理;程式碼是可自行貼到瀏覽器測試的示例,沒有在本機瀏覽器執行。

三種優先級先這樣分#

postTask() 的優先級是刻意粗粒度的,不是精確的毫秒級保證:

優先級適合的工作不要拿來做什麼
user-blocking使用者剛完成的操作、必須儘快反映的互動工作長時間計算、批次分析或不影響目前畫面的同步
user-visible一般畫面更新、使用者可見但不需立即完成的工作把所有任務都塞在這裡,失去分類意義
background預取、索引、統計、快取整理與非必要同步需要在按鈕回應前完成的工作

不指定 priority 時,MDN 文件指出預設是 user-visible。優先級只決定瀏覽器如何安排任務,callback 內如果執行大型同步迴圈,仍然可能阻塞主執行緒。

最基本的 postTask() 用法#

它會回傳一個 Promise,解析值就是 callback 的回傳值:

async function updateSearchIndex(items) {
const result = await scheduler.postTask(
() => buildSearchIndex(items),
{ priority: 'background' },
);
return result;
}

callback 拋出的錯誤會讓 Promise reject,因此應該使用 try/catch.catch()

scheduler
.postTask(() => renderSearchResults(), { priority: 'user-visible' })
.catch((error) => {
console.error('Render task failed', error);
});

這個 API 不是把 callback 丟到另一個 thread;它仍然在瀏覽器的 window 或 worker 排程環境執行。需要真正分離 CPU 工作時,應評估 Web Worker、資料切分與序列化成本。

先做 feature detection#

不要直接寫 scheduler.postTask(),因為 MDN 目前將它標示為 Limited availability。最小檢查可以是:

const supportsPrioritizedTasks =
typeof globalThis.scheduler?.postTask === 'function';
if (supportsPrioritizedTasks) {
scheduler.postTask(() => refreshPreview(), {
priority: 'user-visible',
});
} else {
setTimeout(() => refreshPreview(), 0);
}

fallback 的目標是保持功能正確,不是假裝在舊瀏覽器中重現同一套優先級。若工作本來就可延後,可以在支援的環境使用 requestIdleCallback();若工作需要被拆成多個片段,應使用明確的 chunking,而不是增加一個更大的 timeout。

寫一個保留 Promise 與取消行為的 fallback#

如果你的呼叫端需要統一使用 await,可以把支援檢查包成小型 helper。以下程式碼只在不支援 postTask() 時忽略 priority;它仍保留 delay、callback error 與 AbortSignal 的基本語意:

function scheduleTask(callback, options = {}) {
const { delay = 0, priority = 'user-visible', signal } = options;
if (typeof globalThis.scheduler?.postTask === 'function') {
return globalThis.scheduler.postTask(callback, {
delay,
priority,
signal,
});
}
if (signal?.aborted) {
return Promise.reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
}
return new Promise((resolve, reject) => {
const timer = setTimeout(async () => {
try {
resolve(await callback());
} catch (error) {
reject(error);
}
}, delay);
signal?.addEventListener(
'abort',
() => {
clearTimeout(timer);
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
},
{ once: true },
);
});
}

這不是完整的 scheduler polyfill:fallback 無法重現瀏覽器的 user-blockinguser-visiblebackground 排序,也沒有實作 TaskController 的動態優先級。這樣刻意保留差異,呼叫端才不會把舊瀏覽器中的 timeout 當成同等級保證。

延遲工作與取消工作#

delay 是加入 scheduler queue 前的最小等待時間,實際執行時間可能更晚:

const controller = new AbortController();
const refreshPromise = scheduler.postTask(
() => refreshRecommendations(),
{
priority: 'background',
delay: 2000,
signal: controller.signal,
},
);
// 使用者離開頁面或切換條件後,不再需要這個工作。
controller.abort(new Error('Search context changed'));
refreshPromise.catch((error) => {
if (error.name !== 'AbortError') {
console.error('Background task failed', error);
}
});

如果只需要固定優先級和取消,AbortController 足夠。若要在任務執行前改變優先級,MDN 文件要求不要同時指定 options.priority,而應使用帶有 TaskSignalTaskController;這是另一條 API 路徑,不要把兩種 signal 混寫。

postTask() 不能取代長任務切分#

把一個 500ms 的同步迴圈包在 postTask() 裡,仍然可能讓輸入延遲和畫面卡頓。對大型工作應先切成小批次,必要時在每批之間讓出主執行緒:

async function processInChunks(items, processChunk) {
for (let index = 0; index < items.length; index += 100) {
processChunk(items.slice(index, index + 100));
if (typeof globalThis.scheduler?.yield === 'function') {
await scheduler.yield();
} else {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}

yield()postTask() 的用途不同:postTask() 讓你建立具有優先級、延遲與 signal 的新任務;yield() 適合在同一個 async 工作中暫停後繼續。兩者都不能修復單一 callback 內過大的同步區段,所以 chunk 大小仍要透過實際效能檢查調整。

如何選 API#

需求優先考慮原因
下一個 microtask 立即處理狀態queueMicrotask()不應拿來排背景工作;它可能在瀏覽器重新繪製前持續累積
廣泛支援的稍後執行setTimeout()fallback 可靠,但沒有任務優先級語意
沒有互動壓力時才做的背景工作requestIdleCallback()postTask({ priority: 'background' })仍要設定可接受的延遲與 fallback
希望明確區分互動與背景任務scheduler.postTask()可用 priority、delay、Promise 與 signal 表達意圖
已在 async 函式內,需要把長工作切段scheduler.yield() 或 timeout fallback在同一個流程讓出主執行緒再繼續
需要真正平行處理 CPU 工作Web Worker代價是訊息傳遞、序列化與狀態同步

可以把 postTask() 當成 scheduling policy 的一部分,而不是效能魔法。先定義「哪些工作可延後、哪些工作可取消、哪些工作必須反映在畫面上」,再選 API。

驗證排程是否真的改善體驗#

本文沒有在本機瀏覽器執行。導入專案後至少做以下檢查:

  1. 用 DevTools Performance 錄製互動前後,確認任務是否仍形成長 task。
  2. 用 PerformanceObserver 觀察 longtask,記錄切分前後的 duration。
  3. 在不支援 scheduler 的瀏覽器跑同一組功能測試,確認 fallback 不丟資料、不遺失取消事件。
  4. 測試使用者快速輸入、連續切換頁面與取消 fetch 時,舊任務是否還會覆寫新結果。
  5. 透過實際裝置檢查低階 CPU、背景分頁與電池模式;優先級排序不是所有平台都會有相同體感。

如果你的目標是降低輸入延遲,也可搭配站內的 INP 網頁互動效能優化指南;先找出長任務來源,再決定是切分、延後、取消還是移到 Worker。

常見問題#

Q: scheduler.postTask() 是不是比 setTimeout() 快?#

A: 不應這樣理解。它主要增加任務優先級、延遲、取消與 Promise 語意;實際執行時間仍受瀏覽器、主執行緒負載、頁面狀態與 callback 工作量影響。

Q: background 會保證在瀏覽器閒置時才執行嗎?#

A: 不會。它是粗粒度的背景優先級,不是 requestIdleCallback() 的閒置保證,也不是可以忽略資源量的無限佇列。仍要控制工作大小、取消條件與執行頻率。

Q: 不支援 scheduler 時,可以用 polyfill 完全模擬嗎?#

A: 通常只能保留 Promise、delay 和取消等部分語意,無法用 setTimeout() 完整模擬瀏覽器的優先級排序。應把 fallback 當成正確性路徑,而不是宣稱具有同等效能保證。

Q: postTask() 可以解決所有畫面卡頓嗎?#

A: 不行。過大的同步 callback、昂貴的 layout、過量 DOM 更新與 CPU 密集計算仍需拆分、批次更新、Web Worker 或其他效能策略。先用 Performance 面板找出瓶頸,再調整排程。

參考資料:

MDN:Scheduler.postTask()

MDN:Prioritized Task Scheduling API

Prioritized Task Scheduling API specification

scheduler.postTask 怎麼用?JavaScript 任務優先級與 fallback
https://laplusda.com/posts/javascript-scheduler-posttask-priority/
作者
Zero
發佈於
2026-09-21
許可協議
CC BY-NC-SA 4.0