[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-scheduler-yield-main-thread":32,"quiz-article-scheduler-yield-main-thread":48},[4,20],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Build. Learn. Ship.","Practical courses, engineering-grade articles and open-source tools for people who ship.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Structured paths, listening drills and spaced repetition that actually sticks.","amber-400","⌘",[13,15,29],"es",{"courses":17,"paths":17,"articles":18,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":17},2,{"id":33,"slug":34,"kind":35,"title":36,"description":37,"config":38,"verticalId":5,"vertical":43,"course":40,"_count":44,"access":45,"attempts":47,"questionCount":39},"019fe776-a892-72ec-b654-79ec34073296","scheduler-yield-main-thread","PRACTICE_QUIZ","scheduler.yield() — unblocking the main thread","A long synchronous task holds the main thread and queues every paint and click behind it. scheduler.yield() hands control back mid-task and resumes where it left off. These questions check what the await actually does, why the popular one-line polyfill throws in exactly the browser it was written for, why you should yield on elapsed time rather than item count, where the 50 ms figure comes from, how the continuation's priority compares to postTask, and what yielding costs you.",{"questionCount":39,"timeLimitSec":40,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":41,"maxAttempts":40,"revealAnswers":42,"allowFlagging":18,"allowBacktracking":17},8,null,70,"IMMEDIATE",{"slug":6,"name":7},{"questions":39},{"allowed":17,"reason":46},"FREE",[],{"id":49,"slug":34,"title":50,"subtitle":40,"excerpt":51,"coverUrl":52,"locale":13,"readingMinutes":53,"publishedAt":54,"viewCount":55,"likeCount":19,"commentCount":19,"author":56,"vertical":61,"topic":62,"tags":65,"_count":77,"playground":79,"body":81,"bodyMd":222,"seo":223,"translationGroupId":226,"thread":227,"assessments":229,"translations":232,"quiz":234},"019fe660-c754-703b-80fb-2254bb102ab7","Your UI is freezing because you never gave the browser a break","Long synchronous tasks block the main thread and make your interface unresponsive. `scheduler.yield()` is the native way to hand control back to the browser mid-task so it can paint, handle input, and resume your work exactly where it left off.","\u002Fmedia\u002Fcovers\u002Fscheduler-yield-main-thread.png",4,"2026-08-03T09:54:25.323Z",128,{"id":57,"name":58,"username":59,"avatarUrl":40,"headline":60},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":63,"name":64},"javascript","JavaScript",[66,68,71,74],{"slug":63,"name":67,"color":40},"Javascript",{"slug":69,"name":70,"color":40},"webdev","Webdev",{"slug":72,"name":73,"color":40},"performance","Performance",{"slug":75,"name":76,"color":40},"frontend","Frontend",{"assessments":78},1,{"slug":34,"title":80},"scheduler.yield() — stop freezing the main thread",{"blocks":82,"version":78},[83,87,90,93,98,101,104,107,111,114,120,123,126,129,132,135,138,141,144,147,151,154,157,160,164,167,170,173,177,180,183,186,189,192,195,198,201,204,207,210,213],{"id":84,"html":85,"type":86},"b1","\u003Cp>Every frontend developer has shipped it: a button that doesn&#39;t respond, a list that stutters during a sort, an animation that freezes mid-frame. The cause is almost always the same — a long synchronous task has taken over the main thread, and the browser can&#39;t do anything else until it&#39;s done.\u003C\u002Fp>","paragraph",{"id":88,"html":89,"type":86},"b2","\u003Cp>JavaScript, the render loop, input event handling, and layout all share one thread. When your code runs for more than ~50ms without stopping, the browser queues every pending paint, click, and scroll behind it. Users experience this as lag — or as an app that simply ignores them.\u003C\u002Fp>",{"id":91,"html":92,"type":86},"b3","\u003Cp>The fix is yielding: handing control back to the browser at safe checkpoints so it can catch up before your code continues.\u003C\u002Fp>",{"id":94,"html":95,"text":96,"type":97,"level":31},"b4","Why \u003Ccode>setTimeout(0)\u003C\u002Fcode> isn&#39;t the answer","Why setTimeout(0) isn't the answer","heading",{"id":99,"html":100,"type":86},"b5","\u003Cp>The classic workaround is \u003Ccode>setTimeout(fn, 0)\u003C\u002Fcode> — break work into chunks, schedule each with a zero-delay timeout. It works, but the costs are real. You shatter your logic across multiple callbacks, lose the clean async\u002Fawait call stack, and the &quot;zero&quot; delay isn&#39;t zero — browsers clamp it and the overhead accumulates across hundreds of chunks.\u003C\u002Fp>",{"id":102,"html":103,"type":86},"b6","\u003Cp>\u003Ccode>requestIdleCallback\u003C\u002Fcode> sounds more appropriate (only run during idle time), but idle callbacks can be starved indefinitely when the browser stays busy. It&#39;s unreliable for work that must actually complete.\u003C\u002Fp>",{"id":105,"html":106,"type":86},"b7","\u003Cp>\u003Ccode>requestAnimationFrame\u003C\u002Fcode> is useful for visual tasks synchronized to frames — it&#39;s the wrong tool for a data transform that just happens to be expensive.\u003C\u002Fp>",{"id":108,"html":109,"text":110,"type":97,"level":31},"b8","\u003Ccode>scheduler.yield()\u003C\u002Fcode> — one line","scheduler.yield() — one line",{"id":112,"html":113,"type":86},"b9","\u003Cp>The Scheduler API provides a first-class primitive for this:\u003C\u002Fp>",{"id":115,"code":116,"type":117,"language":118,"highlight":119},"b10","async function processItems(items) {\n  for (let i = 0; i \u003C items.length; i++) {\n    processItem(items[i]);\n\n    if (i % 100 === 0) {\n      await scheduler.yield();\n    }\n  }\n}","code","js",[],{"id":121,"html":122,"type":86},"b11","\u003Cp>That \u003Ccode>await\u003C\u002Fcode> is the entire API. It returns a Promise that resolves in the next task, after the browser has handled pending paints and input events. Your loop resumes exactly where it left off — no callback restructuring, no index bookkeeping, no split logic.\u003C\u002Fp>",{"id":124,"html":125,"type":86},"b12","\u003Cp>The meaningful difference from \u003Ccode>setTimeout(0)\u003C\u002Fcode>: \u003Ccode>scheduler.yield()\u003C\u002Fcode> is \u003Cem>priority-aware\u003C\u002Fem>. When you yield, the scheduler runs higher-priority work first — user input and visual updates — before your continuation. All \u003Ccode>setTimeout\u003C\u002Fcode> callbacks compete at equal priority regardless of urgency.\u003C\u002Fp>",{"id":127,"html":128,"type":86},"b13","\u003C!-- playground:start -->",{"id":130,"html":131,"text":131,"type":97,"level":31},"b14","🎮 Try it yourself",{"id":133,"html":134,"type":86},"b15","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fposts\u002F2026-08-03-scheduler-yield-main-thread\u002Fplayground\u002F\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":136,"html":137,"type":86},"b16","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":139,"html":140,"type":86},"b17","\u003C!-- playground:end -->",{"id":142,"html":143,"text":143,"type":97,"level":31},"b18","Yield on time, not item count",{"id":145,"html":146,"type":86},"b19","\u003Cp>Yielding too rarely means tasks still block; yielding too often adds overhead and slows total completion. The practical target: yield roughly every 50ms of CPU work, the threshold at which browsers flag a task as &quot;long&quot; in DevTools.\u003C\u002Fp>",{"id":148,"code":149,"type":117,"language":118,"highlight":150},"b20","async function processItems(items) {\n  let lastYield = performance.now();\n\n  for (const item of items) {\n    processItem(item);\n\n    if (performance.now() - lastYield > 50) {\n      await scheduler.yield();\n      lastYield = performance.now();\n    }\n  }\n}",[],{"id":152,"html":153,"type":86},"b21","\u003Cp>This yields on elapsed time rather than item count, which is accurate when individual item costs vary. A 10ms item and a 0.1ms item both need different yield frequencies — time-based checking handles both without tuning.\u003C\u002Fp>",{"id":155,"html":156,"text":156,"type":97,"level":31},"b22","A real-world pattern: progressive table rendering",{"id":158,"html":159,"type":86},"b23","\u003Cp>Loading a large dataset, building DOM nodes, and inserting them into a table is one of the most common long-task offenders. With \u003Ccode>scheduler.yield()\u003C\u002Fcode>, the table builds progressively and the page stays interactive throughout:\u003C\u002Fp>",{"id":161,"code":162,"type":117,"language":118,"highlight":163},"b24","async function renderTable(rows) {\n  const fragment = document.createDocumentFragment();\n  let lastYield = performance.now();\n\n  for (const row of rows) {\n    fragment.appendChild(buildRow(row));\n\n    if (performance.now() - lastYield > 50) {\n      table.appendChild(fragment.cloneNode(true)); \u002F\u002F flush what we have\n      fragment.replaceChildren();\n      await scheduler.yield();\n      lastYield = performance.now();\n    }\n  }\n\n  table.appendChild(fragment); \u002F\u002F flush remainder\n}",[],{"id":165,"html":166,"type":86},"b25","\u003Cp>A user who clicks a button while this loop runs will see that click handled immediately — it&#39;s no longer lost to a blocked thread.\u003C\u002Fp>",{"id":168,"html":169,"text":169,"type":97,"level":31},"b26","Browser support, and the one detail that breaks the polyfill",{"id":171,"html":172,"type":86},"b27","\u003Cp>\u003Ccode>scheduler.yield()\u003C\u002Fcode> is in Chrome 129 and Firefox 142. \u003Cstrong>Safari has not shipped it\u003C\u002Fstrong> — so this is not Baseline, and a bare \u003Ccode>scheduler.yield()\u003C\u002Fcode> is not something you ship unguarded. You need a fallback:\u003C\u002Fp>",{"id":174,"code":175,"type":117,"language":118,"highlight":176},"b28","const yieldToMain = globalThis.scheduler?.yield?.bind(globalThis.scheduler)\n  ?? (() => new Promise((resolve) => setTimeout(resolve, 0)));",[],{"id":178,"html":179,"type":86},"b29","\u003Cp>That \u003Ccode>globalThis.\u003C\u002Fcode> prefix is the whole ballgame, and it is the detail almost every snippet of this polyfill gets wrong. Writing \u003Ccode>scheduler?.yield?.bind(scheduler)\u003C\u002Fcode> looks safer — optional chaining, after all — but optional chaining only guards against \u003Ccode>null\u003C\u002Fcode> and \u003Ccode>undefined\u003C\u002Fcode> \u003Cem>values\u003C\u002Fem>. It does nothing for an identifier that was never declared. In Safari there is no \u003Ccode>scheduler\u003C\u002Fcode> global at all, so evaluating the bare word \u003Ccode>scheduler\u003C\u002Fcode> throws \u003Ccode>ReferenceError: Can&#39;t find variable: scheduler\u003C\u002Fcode> before \u003Ccode>?.\u003C\u002Fcode> is ever consulted. The polyfill written to protect Safari crashes only in Safari.\u003C\u002Fp>",{"id":181,"html":182,"type":86},"b30","\u003Cp>\u003Ccode>globalThis.scheduler\u003C\u002Fcode> is a property lookup on an object that definitely exists, so it evaluates to \u003Ccode>undefined\u003C\u002Fcode> and \u003Ccode>?.\u003C\u002Fcode> short-circuits exactly as intended.\u003C\u002Fp>",{"id":184,"html":185,"type":86},"b31","\u003Cp>Replace \u003Ccode>scheduler.yield()\u003C\u002Fcode> with \u003Ccode>yieldToMain()\u003C\u002Fcode> throughout your code. In Chrome and Firefox you get priority-aware yielding; everywhere else you get the \u003Ccode>setTimeout\u003C\u002Fcode> fallback, which still unblocks the thread — it just isn&#39;t priority-aware. The async structure is identical either way.\u003C\u002Fp>",{"id":187,"html":188,"type":86},"b32","\u003C!-- quiz:start -->",{"id":190,"html":191,"text":191,"type":97,"level":31},"b33","🧠 Test yourself",{"id":193,"html":194,"type":86},"b34","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fquiz\u002Ftake.html?post=2026-08-03-scheduler-yield-main-thread\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":196,"html":197,"type":86},"b35","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":199,"html":200,"type":86},"b36","\u003C!-- quiz:end -->",{"id":202,"html":203,"text":203,"type":97,"level":31},"b37","The takeaway",{"id":205,"html":206,"type":86},"b38","\u003Cp>Open DevTools → Performance, record a slow interaction, and look for the red &quot;Long task&quot; markers. Each one is a stretch where your code held the thread and blocked the browser. Pick the longest offender, find the hot loop inside it, and add \u003Ccode>await yieldToMain()\u003C\u002Fcode> at a logical checkpoint — a time-based check works well. That one await is often the difference between an app that feels broken and one that feels instant.\u003C\u002Fp>",{"id":208,"type":209},"b39","divider",{"id":211,"html":212,"type":86},"b40","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":214,"type":215,"items":216,"ordered":18},"b41","list",[217,218,219,220,221],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>","Every frontend developer has shipped it: a button that doesn't respond, a list that stutters during a sort, an animation that freezes mid-frame. The cause is almost always the same — a long synchronous task has taken over the main thread, and the browser can't do anything else until it's done.\n\nJavaScript, the render loop, input event handling, and layout all share one thread. When your code runs for more than ~50ms without stopping, the browser queues every pending paint, click, and scroll behind it. Users experience this as lag — or as an app that simply ignores them.\n\nThe fix is yielding: handing control back to the browser at safe checkpoints so it can catch up before your code continues.\n\n## Why `setTimeout(0)` isn't the answer\n\nThe classic workaround is `setTimeout(fn, 0)` — break work into chunks, schedule each with a zero-delay timeout. It works, but the costs are real. You shatter your logic across multiple callbacks, lose the clean async\u002Fawait call stack, and the \"zero\" delay isn't zero — browsers clamp it and the overhead accumulates across hundreds of chunks.\n\n`requestIdleCallback` sounds more appropriate (only run during idle time), but idle callbacks can be starved indefinitely when the browser stays busy. It's unreliable for work that must actually complete.\n\n`requestAnimationFrame` is useful for visual tasks synchronized to frames — it's the wrong tool for a data transform that just happens to be expensive.\n\n## `scheduler.yield()` — one line\n\nThe Scheduler API provides a first-class primitive for this:\n\n```js\nasync function processItems(items) {\n  for (let i = 0; i \u003C items.length; i++) {\n    processItem(items[i]);\n\n    if (i % 100 === 0) {\n      await scheduler.yield();\n    }\n  }\n}\n```\n\nThat `await` is the entire API. It returns a Promise that resolves in the next task, after the browser has handled pending paints and input events. Your loop resumes exactly where it left off — no callback restructuring, no index bookkeeping, no split logic.\n\nThe meaningful difference from `setTimeout(0)`: `scheduler.yield()` is *priority-aware*. When you yield, the scheduler runs higher-priority work first — user input and visual updates — before your continuation. All `setTimeout` callbacks compete at equal priority regardless of urgency.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fposts\u002F2026-08-03-scheduler-yield-main-thread\u002Fplayground\u002F)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Yield on time, not item count\n\nYielding too rarely means tasks still block; yielding too often adds overhead and slows total completion. The practical target: yield roughly every 50ms of CPU work, the threshold at which browsers flag a task as \"long\" in DevTools.\n\n```js\nasync function processItems(items) {\n  let lastYield = performance.now();\n\n  for (const item of items) {\n    processItem(item);\n\n    if (performance.now() - lastYield > 50) {\n      await scheduler.yield();\n      lastYield = performance.now();\n    }\n  }\n}\n```\n\nThis yields on elapsed time rather than item count, which is accurate when individual item costs vary. A 10ms item and a 0.1ms item both need different yield frequencies — time-based checking handles both without tuning.\n\n## A real-world pattern: progressive table rendering\n\nLoading a large dataset, building DOM nodes, and inserting them into a table is one of the most common long-task offenders. With `scheduler.yield()`, the table builds progressively and the page stays interactive throughout:\n\n```js\nasync function renderTable(rows) {\n  const fragment = document.createDocumentFragment();\n  let lastYield = performance.now();\n\n  for (const row of rows) {\n    fragment.appendChild(buildRow(row));\n\n    if (performance.now() - lastYield > 50) {\n      table.appendChild(fragment.cloneNode(true)); \u002F\u002F flush what we have\n      fragment.replaceChildren();\n      await scheduler.yield();\n      lastYield = performance.now();\n    }\n  }\n\n  table.appendChild(fragment); \u002F\u002F flush remainder\n}\n```\n\nA user who clicks a button while this loop runs will see that click handled immediately — it's no longer lost to a blocked thread.\n\n## Browser support, and the one detail that breaks the polyfill\n\n`scheduler.yield()` is in Chrome 129 and Firefox 142. **Safari has not shipped it** — so this is not Baseline, and a bare `scheduler.yield()` is not something you ship unguarded. You need a fallback:\n\n```js\nconst yieldToMain = globalThis.scheduler?.yield?.bind(globalThis.scheduler)\n  ?? (() => new Promise((resolve) => setTimeout(resolve, 0)));\n```\n\nThat `globalThis.` prefix is the whole ballgame, and it is the detail almost every snippet of this polyfill gets wrong. Writing `scheduler?.yield?.bind(scheduler)` looks safer — optional chaining, after all — but optional chaining only guards against `null` and `undefined` *values*. It does nothing for an identifier that was never declared. In Safari there is no `scheduler` global at all, so evaluating the bare word `scheduler` throws `ReferenceError: Can't find variable: scheduler` before `?.` is ever consulted. The polyfill written to protect Safari crashes only in Safari.\n\n`globalThis.scheduler` is a property lookup on an object that definitely exists, so it evaluates to `undefined` and `?.` short-circuits exactly as intended.\n\nReplace `scheduler.yield()` with `yieldToMain()` throughout your code. In Chrome and Firefox you get priority-aware yielding; everywhere else you get the `setTimeout` fallback, which still unblocks the thread — it just isn't priority-aware. The async structure is identical either way.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fquiz\u002Ftake.html?post=2026-08-03-scheduler-yield-main-thread)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n## The takeaway\n\nOpen DevTools → Performance, record a slow interaction, and look for the red \"Long task\" markers. Each one is a stretch where your code held the thread and blocked the browser. Pick the longest offender, find the hot loop inside it, and add `await yieldToMain()` at a logical checkpoint — a time-based check works well. That one await is often the difference between an app that feels broken and one that feels instant.\n\n---\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":50,"canonical":224,"description":225},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fscheduler-yield-main-thread","Long synchronous tasks block the main thread and make your interface unresponsive. `scheduler.yield()` is the native way to hand control back to the browser mid-task so it can pain","019fe660-c754-703b-80fb-24df4b887c55",{"id":228,"locked":18},"019fe660-ce50-74d1-8f30-ec29238bcefb",[230],{"id":33,"slug":34,"title":36,"_count":231},{"questions":39},[233],{"locale":13,"slug":34},{"id":33,"slug":34,"title":36,"_count":235,"questionCount":39},{"questions":39}]