惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
D
DataBreaches.Net
F
Fortinet All Blogs
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Apple Machine Learning Research
Apple Machine Learning Research
H
Help Net Security
M
MIT News - Artificial intelligence
美团技术团队
人人都是产品经理
人人都是产品经理
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Cloudflare Blog
有赞技术团队
有赞技术团队
L
LangChain Blog
博客园 - Franky
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 【当耐特】
S
SegmentFault 最新的问题
V
Visual Studio Blog
Blog — PlanetScale
Blog — PlanetScale
Hugging Face - Blog
Hugging Face - Blog
B
Blog
I
InfoQ

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
scheduler.yield(): The One-Liner That Fixes Your INP
Danny Holloran · 2026-06-29 · via DEV Community

Originally published on danholloran.me


You click a button, and for a beat nothing happens. The spinner doesn't appear, the page feels stuck, and then everything updates at once. That little hitch is exactly what Interaction to Next Paint measures, and the usual culprit is a long task: a chunk of JavaScript that hogs the main thread so the browser can't paint or respond to input until it finishes.

The old advice was to slice that work into smaller pieces with setTimeout, handing control back to the browser between chunks. It works, but it comes with a tax. scheduler.yield() does the same job without the tax, and it's now available across most browsers. If you've been ignoring it, this is the post that should change your mind.

Why setTimeout yielding hurts

The classic pattern looks like this. You're processing a big array, so you periodically yield:

async function processItems(items) {
  for (const [i, item] of items.entries()) {
    doExpensiveWork(item);
    if (i % 50 === 0) {
      await new Promise((resolve) => setTimeout(resolve, 0));
    }
  }
}

This breaks the long task into shorter ones, which is genuinely good for input delay. The problem is where your continuation lands. When you yield with setTimeout, the rest of your function goes to the back of the task queue. Anything else that got scheduled in the meantime, a third-party analytics callback, another component's work, a different setTimeout, now runs before you get control back. Your loop can stall behind work you don't care about, and a job that should take 100ms stretches out unpredictably.

You yielded to be polite, and the browser took you a little too literally.

What scheduler.yield() does differently

scheduler.yield() returns a promise you can await. Execution pauses at that point and hands the main thread back, exactly like the setTimeout trick, so pending interactions can be serviced. The difference is the continuation gets put in a prioritized queue. When the browser comes back around, your function resumes before other similar tasks that were waiting, rather than after them.

Rewriting the loop is almost anticlimactic:

async function processItems(items) {
  for (const [i, item] of items.entries()) {
    doExpensiveWork(item);
    if (i % 50 === 0) {
      await scheduler.yield();
    }
  }
}

Same shape, better behavior. You still let high-priority interaction work jump the line, the whole point of yielding, but your own continuation isn't shoved to the end of an unbounded queue. In Chrome's framing, a scheduler.yield() continuation outranks a scheduler.postTask() task of the same priority level, which is what keeps your loop from getting starved.

A common real-world shape is an event handler that needs to show feedback before doing slow work:

button.addEventListener("click", async () => {
  showSpinner();
  await scheduler.yield(); // let the browser paint the spinner
  doSlowContentSwap(); // then run the expensive part
});

Without the yield, the spinner and the slow swap are one long task, so the spinner never actually appears until the work is already done.

Shipping it without breaking Safari

The honest caveat: scheduler.yield() is a Chromium feature. It's been stable in Chrome and Edge since version 129 (September 2024) and covers roughly 70% of global traffic, but Safari and Firefox don't ship it yet, so you can't call it blind. Feature-detect and fall back.

A tidy inline fallback covers you:

function yieldToMain() {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

Now await yieldToMain() gives you the prioritized continuation where it's supported and the plain setTimeout behavior everywhere else. Non-Chromium users still get the benefit of broken-up tasks; they just don't get the queue-jumping bonus.

If you'd rather not hand-roll it, Google Chrome Labs publishes a scheduler-polyfill package that implements the whole Scheduler API. It backs scheduler.yield() with user-blocking postTask() tasks where available and falls through to setTimeout, MessageChannel, and requestIdleCallback otherwise, so you get consistent semantics across browsers from a single import.

Where to actually reach for it

This isn't a sprinkle-everywhere API. The wins come from places where you genuinely run long synchronous-ish work in response to input: rendering a large list after a click, parsing or transforming a big payload, hydrating a heavy widget, or running a sequence of independent setup steps on load. Drop a yield between logical chunks and measure INP before and after with something like the web-vitals library or your RUM tooling.

The pattern to internalize is simple: when a single task does too much, slice it, and when you slice it, yield with scheduler.yield() rather than setTimeout so your own work doesn't pay the price for being considerate. It's one of the rare performance fixes that's both a real improvement and a one-line diff.


This post was originally published on danholloran.me. Follow along there for more frontend and dev content.