Scheduler API in Practice: Browser Task Priority Scheduling and Main Thread Optimization

性能优化(Updated Jun 12, 2026)

The Main Thread Scheduling Dilemma

The browser main thread handles UI rendering, event processing, and JavaScript execution simultaneously. Long tasks (>50ms) block interaction and degrade INP:

Traditional Approach Problem
setTimeout(fn, 0) Actual delay 4ms, no priority control
requestIdleCallback Only runs when idle, no timeliness guarantee
await scheduler.yield() ✅ Precise yielding, maintains scheduling order
scheduler.postTask() Priority + yielding + cancellation = complete scheduling

Scheduler API Priority Model

scheduler.postTask() provides three priority levels mapped to browser internal scheduling:

Priority Meaning Typical Use Chrome Internal Mapping
user-blocking Highest, execute immediately User input response, animation Very High
user-visible Medium, execute soon DOM updates, data parsing High / Medium
background Lowest, execute when idle Analytics, pre-computation Low / Best Effort

Basic Usage

const result = await scheduler.postTask(
  () => heavyComputation(data),
  { priority: 'background' }
);

Practice: Priority Scheduling for Image Compression

ToolsKu's Image Compress needs to respond to user interaction while batch-processing images:

async function handleBatchCompress(files: File[]) {
  const controller = new TaskController({ priority: 'background' });

  for (const file of files) {
    // Low priority: compression doesn't block UI
    scheduler.postTask(
      async () => {
        const result = await compressImage(file, { quality: 0.85 });
        // Medium priority: update DOM so user sees progress
        await scheduler.postTask(
          () => updateProgressUI(file.name, result),
          { priority: 'user-visible' }
        );
      },
      { signal: controller.signal }
    );
  }

  return controller;
}

When the user clicks "Cancel", controller.abort() immediately stops all pending tasks.


scheduler.yield(): Precise Main Thread Yielding

scheduler.yield() is more precise than setTimeout(fn, 0)—it yields to higher-priority tasks while maintaining execution order:

async function processLargeDataset(items: Item[]) {
  const results: Result[] = [];

  for (let i = 0; i < items.length; i++) {
    results.push(transform(items[i]));

    // Yield every 50 items to avoid long tasks
    if (i % 50 === 0) {
      await scheduler.yield();
    }
  }

  return results;
}

yield() vs setTimeout() Comparison

Feature setTimeout(fn, 0) scheduler.yield()
Minimum delay 4ms 0ms
Priority-aware ❌ No ✅ Yields to higher priority
Execution order Not guaranteed ✅ Maintains scheduling order
Long task splitting
Browser support All Chrome 115+

TaskController: Cancellation and Dynamic Priority

const controller = new TaskController({ priority: 'user-visible' });

// Submit task
scheduler.postTask(() => renderChart(data), { signal: controller.signal });

// Dynamic downgrade: lower priority when tab is hidden
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    controller.setPriority('background');
  } else {
    controller.setPriority('user-visible');
  }
});

// Cancel all tasks
controller.abort();

Strategies for Avoiding Long Tasks

Strategy 1: Chunking + 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();
  }
}

Strategy 2: Priority Layering

function scheduleWork(work: WorkItem) {
  const priority = work.type === 'user-input' ? 'user-blocking'
    : work.type === 'render' ? 'user-visible'
    : 'background';

  return scheduler.postTask(work.execute, { priority });
}

Strategy 3: AbortController Integration

const ac = new AbortController();

scheduler.postTask(() => mergePDFs(files), {
  priority: 'background',
  signal: ac.signal
});

// Cancel when user navigates away
window.addEventListener('beforeunload', () => ac.abort());

Performance Benchmark: PDF Merge Scenario

Benchmark for PDF Merge processing 20 PDF files:

Approach Total Time INP (ms) Main Thread Blocking
Synchronous loop 3.2s 480 3.2s
setTimeout chunking 3.3s 120 0.4s
postTask + yield 3.2s 60 0.1s

Total time unchanged, but INP drops from 480ms to 60ms—8x better interaction responsiveness.


Compatibility and Fallback

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());
}

Common Questions

postTask or requestIdleCallback?

requestIdleCallback only runs when the browser is idle—suitable for non-urgent work (e.g., preloading). postTask with background priority is similar but more controllable, with cancellation and dynamic priority adjustment.

Does yield() increase total time?

A single yield() costs ~0.1ms. Yielding every 50 items for 10,000 items adds only 20ms overhead—far less than the blocking time avoided.

How to debug priorities?

Chrome DevTools → Performance panel shows task priority annotations. You can also trace scheduling with scheduler.postTask(() => console.trace(), { priority: 'background' }).


Summary

The Scheduler API provides precise priority scheduling for the browser main thread. scheduler.postTask() enables three-level priority task dispatch, scheduler.yield() precisely yields the main thread to avoid long tasks, and TaskController supports cancellation and dynamic priority adjustment. This is essential infrastructure for building high-performance, highly responsive web applications.

Try these browser-local tools — no sign-up required →

#Scheduler API#postTask#优先级#主线程#任务调度