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-blocking、user-visible、background 排序,也沒有實作 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,而應使用帶有 TaskSignal 的 TaskController;這是另一條 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。
驗證排程是否真的改善體驗
本文沒有在本機瀏覽器執行。導入專案後至少做以下檢查:
- 用 DevTools Performance 錄製互動前後,確認任務是否仍形成長 task。
- 用 PerformanceObserver 觀察 longtask,記錄切分前後的 duration。
- 在不支援
scheduler的瀏覽器跑同一組功能測試,確認 fallback 不丟資料、不遺失取消事件。 - 測試使用者快速輸入、連續切換頁面與取消 fetch 時,舊任務是否還會覆寫新結果。
- 透過實際裝置檢查低階 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 面板找出瓶頸,再調整排程。
參考資料: