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

性能优化

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#优先级#主线程#任务调度