[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-scheduler-yield-main-thread":32},[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,{"slug":33,"title":34,"summary":35,"html":36,"css":37,"js":38,"config":39,"doc":41},"scheduler-yield-main-thread","scheduler.yield() — stop freezing the main thread",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🎮 Give the browser a break\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      The same CPU work, run three ways. The blue dot is driven by\n      \u003Ccode>requestAnimationFrame\u003C\u002Fcode>, so it only moves when the browser gets a\n      turn — \u003Cstrong>if it stops, your thread is blocked\u003C\u002Fstrong>. Run each mode and\n      watch the dot, then try clicking the button mid-run.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"banner info\" id=\"support\">\u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Pick a mode and run\u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\">\n        \u003Cbutton class=\"btn bad\"     id=\"runBlock\">Block the thread\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn\"         id=\"runTimeout\">Yield with setTimeout(0)\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn primary\" id=\"runYield\">Yield with scheduler.yield()\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"wrap-controls\" style=\"margin-top:16px\">\n        \u003Cdiv class=\"field\">\n          \u003Clabel class=\"label\" for=\"load\">Workload \u003Cspan class=\"mono\" id=\"loadOut\">600 ms\u003C\u002Fspan>\u003C\u002Flabel>\n          \u003Cinput type=\"range\" id=\"load\" min=\"200\" max=\"2000\" step=\"100\" value=\"600\" \u002F>\n        \u003C\u002Fdiv>\n        \u003Cdiv class=\"field\">\n          \u003Clabel class=\"label\" for=\"budget\">Yield every \u003Cspan class=\"mono\" id=\"budgetOut\">50 ms\u003C\u002Fspan>\u003C\u002Flabel>\n          \u003Cinput type=\"range\" id=\"budget\" min=\"4\" max=\"200\" step=\"2\" value=\"50\" \u002F>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\">\n        Workload is calibrated to real CPU time on \u003Cem>your\u003C\u002Fem> machine, so 600&nbsp;ms means\n        roughly 600&nbsp;ms of actual arithmetic. The yield budget only affects the two\n        yielding modes — it is the \u003Ccode>&gt; 50\u003C\u002Fcode> in\n        \u003Ccode>if (performance.now() - lastYield &gt; 50)\u003C\u002Fcode>.\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> Is the browser still alive?\u003C\u002Fdiv>\n      \u003Cdiv class=\"track\">\n        \u003Cdiv class=\"runner\" id=\"runner\">\u003C\u002Fdiv>\n        \u003Cspan class=\"track-label\" id=\"fps\">idle\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"clicker\" style=\"margin-top:14px\">\n        \u003Cbutton class=\"btn sm\" id=\"clickMe\">Click me during the run\u003C\u002Fbutton>\n        \u003Cspan class=\"count\" id=\"clickCount\">0 clicks · worst delay —\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"metrics\">\n        \u003Cdiv class=\"metric\">\n          \u003Cspan class=\"k\">frames painted\u003C\u002Fspan>\n          \u003Cspan class=\"v\" id=\"mFrames\">—\u003C\u002Fspan>\n        \u003C\u002Fdiv>\n        \u003Cdiv class=\"metric\">\n          \u003Cspan class=\"k\">longest freeze\u003C\u002Fspan>\n          \u003Cspan class=\"v\" id=\"mFreeze\">—\u003C\u002Fspan>\n        \u003C\u002Fdiv>\n        \u003Cdiv class=\"metric\">\n          \u003Cspan class=\"k\">total time\u003C\u002Fspan>\n          \u003Cspan class=\"v\" id=\"mTotal\">—\u003C\u002Fspan>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"status\" id=\"status\" style=\"margin-top:14px\">\n        \u003Cspan class=\"dot info\" id=\"dot\">\u003C\u002Fspan>\u003Cspan id=\"statusText\">Ready — pick a mode above.\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"hint\">\n        “Longest freeze” is the biggest gap between two animation frames. Anything over\n        ~50&nbsp;ms is what Chrome DevTools flags as a long task; over ~200&nbsp;ms and users\n        assume your app is broken.\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">3\u003C\u002Fspan> Compare your runs\u003C\u002Fdiv>\n      \u003Ctable class=\"results\">\n        \u003Cthead>\n          \u003Ctr>\u003Cth>mode\u003C\u002Fth>\u003Cth>frames\u003C\u002Fth>\u003Cth>longest freeze\u003C\u002Fth>\u003Cth>clicks heard\u003C\u002Fth>\u003Cth>total\u003C\u002Fth>\u003C\u002Ftr>\n        \u003C\u002Fthead>\n        \u003Ctbody id=\"tbody\">\u003C\u002Ftbody>\n      \u003C\u002Ftable>\n      \u003Cp class=\"hint\">\n        Run all three with the same workload. Blocking is the one that should look obviously\n        broken: a single frame and one enormous freeze. Both yielding modes keep the worst\n        freeze near your budget instead. Between those two, expect\n        \u003Ccode>scheduler.yield()\u003C\u002Fcode> to \u003Cem>finish sooner\u003C\u002Fem> — browsers clamp nested\n        \u003Ccode>setTimeout\u003C\u002Fcode> to roughly 4&nbsp;ms per hop, and across hundreds of hops that\n        adds up, while a yield continuation resumes in a boosted task queue.\n        \u003Ccode>setTimeout(0)\u003C\u002Fcode> may well paint more frames along the way; it is buying them\n        with wall-clock time you did not choose to spend.\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">4\u003C\u002Fspan> The loop that is running\u003C\u002Fdiv>\n      \u003Cdiv class=\"code\" id=\"code\" aria-live=\"polite\">\u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"codeHint\">The highlighted line is the only difference between the modes.\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>","\u002F*\n * Shared playground design system — daily-post\n * ============================================\n * Link it from any post's playground\u002Findex.html (no per-post boilerplate):\n *\n *   \u003Clink rel=\"stylesheet\" href=\"..\u002F..\u002F..\u002Fplayground\u002Fassets\u002Fbase.css\" \u002F>\n *\n * The relative path resolves the same locally (posts\u002F\u003Cbase>\u002Fplayground\u002F) and on\n * the deployed host (public\u002Fposts\u002F\u003Cbase>\u002Fplayground\u002F), because build-site.js\n * mirrors this file to public\u002Fplayground\u002Fassets\u002Fbase.css. For a CodeSandbox\n * --embed, playground.js inlines this file so the single-file sandbox still styles.\n *\n * Then write ONLY the demo-specific CSS in your inline \u003Cstyle>. Everything below\n * — tokens, layout, panels, buttons, toggles, sliders, code blocks, status\u002Flog,\n * banners, presets — comes for free and looks consistent across every post.\n *\n * Theme-aware: dark by default, light via prefers-color-scheme AND a\n * [data-theme] override on \u003Chtml> (so a toggle can force either). Accessible:\n * real focus rings, adequate contrast, reduced-motion honored.\n *\u002F\n\n\u002F* ---------- design tokens ---------- *\u002F\n:root {\n  --bg: #0d1117;\n  --bg-grid: #ffffff0a;\n  --panel: #161b22;\n  --panel-2: #0b0e14;\n  --card: #0b0e14;\n  --ink: #e8ebf2;\n  --muted: #9aa4b2;\n  --faint: #6b7688;\n  --line: #232a35;\n  --line-soft: #ffffff12;\n  --accent: #7c9cff;\n  --accent-ink: #0b0e14;\n  --good: #3fb950;\n  --warn: #f0883e;\n  --bad: #f85149;\n  --info: #58a6ff;\n  --radius: 14px;\n  --radius-sm: 9px;\n  --shadow: 0 8px 30px #0006;\n  --shadow-sm: 0 4px 14px #0005;\n  --mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n  --sans: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif;\n  color-scheme: dark;\n}\n\n\u002F* Light theme — system preference, overridable by [data-theme] on \u003Chtml>. *\u002F\n@media (prefers-color-scheme: light) {\n  :root:not([data-theme=\"dark\"]) {\n    --bg: #f4f6fb;\n    --bg-grid: #00000008;\n    --panel: #ffffff;\n    --panel-2: #f0f3f9;\n    --card: #f7f9fc;\n    --ink: #131722;\n    --muted: #5b6472;\n    --faint: #8a93a3;\n    --line: #e2e7f0;\n    --line-soft: #00000010;\n    --accent: #2f6df0;\n    --accent-ink: #ffffff;\n    --good: #1a7f37;\n    --warn: #bc4c00;\n    --bad: #cf222e;\n    --info: #0969da;\n    --shadow: 0 8px 30px #0000001f;\n    --shadow-sm: 0 4px 14px #00000014;\n    color-scheme: light;\n  }\n}\n:root[data-theme=\"light\"] {\n  --bg: #f4f6fb;\n  --bg-grid: #00000008;\n  --panel: #ffffff;\n  --panel-2: #f0f3f9;\n  --card: #f7f9fc;\n  --ink: #131722;\n  --muted: #5b6472;\n  --faint: #8a93a3;\n  --line: #e2e7f0;\n  --line-soft: #00000010;\n  --accent: #2f6df0;\n  --accent-ink: #ffffff;\n  --good: #1a7f37;\n  --warn: #bc4c00;\n  --bad: #cf222e;\n  --info: #0969da;\n  --shadow: 0 8px 30px #0000001f;\n  --shadow-sm: 0 4px 14px #00000014;\n  color-scheme: light;\n}\n\n\u002F* ---------- reset & base ---------- *\u002F\n*, *::before, *::after { box-sizing: border-box; }\n\nbody {\n  margin: 0;\n  min-height: 100vh;\n  font-family: var(--sans);\n  color: var(--ink);\n  background:\n    radial-gradient(circle at 1px 1px, var(--bg-grid) 1px, transparent 0) 0 0 \u002F 24px 24px,\n    var(--bg);\n  display: flex;\n  justify-content: center;\n  align-items: flex-start;\n  padding: clamp(14px, 3vw, 26px);\n  line-height: 1.55;\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n}\n\n\u002F* The single content column. *\u002F\n.pg {\n  width: 100%;\n  max-width: 860px;\n}\n\n\u002F* ---------- header ---------- *\u002F\n.pg-title {\n  font-size: clamp(1.15rem, 1rem + 1vw, 1.4rem);\n  font-weight: 800;\n  letter-spacing: -0.02em;\n  margin: 0 0 4px;\n  display: flex;\n  align-items: center;\n  gap: 9px;\n}\n.pg-sub {\n  color: var(--muted);\n  font-size: 0.92rem;\n  line-height: 1.5;\n  margin: 0 0 16px;\n  max-width: 66ch;\n}\n.pg-sub strong, .pg-sub b { color: var(--ink); font-weight: 700; }\n.pg-sub code { font-family: var(--mono); background: var(--line-soft); color: var(--accent);\n  padding: 1px 6px; border-radius: 6px; font-size: 0.85em; }\n\n\u002F* ---------- panels ---------- *\u002F\n.panel {\n  background: var(--panel);\n  border: 1px solid var(--line);\n  border-radius: var(--radius);\n  padding: 16px 18px;\n  margin-bottom: 14px;\n  box-shadow: var(--shadow-sm);\n}\n.panel-label {\n  font-size: 0.7rem;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  color: var(--muted);\n  font-weight: 800;\n  margin: 0 0 13px;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.panel-label .n {\n  display: inline-grid; place-items: center;\n  width: 18px; height: 18px; border-radius: 50%;\n  background: var(--accent); color: var(--accent-ink);\n  font-size: 0.66rem; font-weight: 800;\n}\n\n\u002F* ---------- layout helpers ---------- *\u002F\n.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }\n.row.between { justify-content: space-between; }\n.col { display: flex; flex-direction: column; gap: 10px; }\n.grow { flex: 1 1 0; min-width: 0; }\n.wrap-controls { display: flex; flex-wrap: wrap; gap: 16px 24px; align-items: flex-end; }\n.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.field > .label, .ctl-label {\n  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;\n  color: var(--muted); font-weight: 800;\n}\n\n\u002F* ---------- buttons ---------- *\u002F\n.btn {\n  font: inherit; font-weight: 650; font-size: 0.88rem;\n  cursor: pointer; user-select: none;\n  border: 1px solid var(--line);\n  background: var(--card); color: var(--ink);\n  padding: 9px 15px; border-radius: var(--radius-sm);\n  display: inline-flex; align-items: center; gap: 8px;\n  transition: background .15s ease, border-color .15s ease, transform .05s ease, color .15s ease;\n}\n.btn:hover { background: var(--line-soft); border-color: var(--muted); }\n.btn:active { transform: translateY(1px); }\n.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n.btn[disabled] { opacity: 0.45; cursor: not-allowed; }\n.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }\n.btn.primary:hover { filter: brightness(1.08); background: var(--accent); }\n.btn.good { background: var(--good); border-color: var(--good); color: #04140a; }\n.btn.bad  { background: var(--bad);  border-color: var(--bad);  color: #fff; }\n.btn.ghost { background: transparent; }\n.btn.sm { padding: 6px 11px; font-size: 0.82rem; }\n\n.btn-row { display: flex; flex-wrap: wrap; gap: 9px; }\n\n\u002F* Segmented control — a row of buttons where one is active (aria-pressed). *\u002F\n.seg { display: inline-flex; background: var(--card); border: 1px solid var(--line);\n  border-radius: var(--radius-sm); padding: 3px; gap: 3px; }\n.seg button {\n  font: inherit; font-weight: 650; font-size: 0.84rem; cursor: pointer;\n  border: none; background: transparent; color: var(--muted);\n  padding: 7px 13px; border-radius: 7px;\n  transition: background .15s ease, color .15s ease;\n}\n.seg button:hover { color: var(--ink); }\n.seg button[aria-pressed=\"true\"] { background: var(--accent); color: var(--accent-ink); }\n.seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* Preset chips — worked-example shortcuts. *\u002F\n.presets { display: flex; flex-wrap: wrap; gap: 8px; }\n.presets .btn { background: var(--panel-2); }\n\n\u002F* ---------- toggle switch ---------- *\u002F\n.toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer;\n  font-size: 0.88rem; font-weight: 600; user-select: none; }\n.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }\n.switch { position: relative; width: 44px; height: 25px; border-radius: 999px; flex: none;\n  background: var(--line); border: 1px solid var(--line); transition: background .18s ease; }\n.switch::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 19px; height: 19px;\n  border-radius: 50%; background: #cfd6e4; transition: transform .18s ease; box-shadow: 0 1px 3px #0006; }\n.toggle input:checked + .switch { background: var(--good); border-color: var(--good); }\n.toggle input:checked + .switch::after { transform: translateX(19px); background: #fff; }\n.toggle input:focus-visible + .switch { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* ---------- range slider ---------- *\u002F\ninput[type=\"range\"] {\n  -webkit-appearance: none; appearance: none; width: 100%; height: 6px;\n  background: var(--line); border-radius: 999px; outline: none; cursor: pointer; margin: 8px 0;\n}\ninput[type=\"range\"]::-webkit-slider-thumb {\n  -webkit-appearance: none; appearance: none; width: 19px; height: 19px; border-radius: 50%;\n  background: var(--accent); border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]::-moz-range-thumb {\n  width: 19px; height: 19px; border-radius: 50%; background: var(--accent);\n  border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }\n\n\u002F* ---------- text inputs \u002F selects ---------- *\u002F\ninput[type=\"text\"], input[type=\"number\"], input[type=\"search\"], select, textarea {\n  font: inherit; color: var(--ink); background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 8px 11px; width: 100%;\n}\ntextarea { font-family: var(--mono); font-size: 0.85rem; line-height: 1.6; resize: vertical; }\ninput:focus-visible, select:focus-visible, textarea:focus-visible {\n  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);\n}\n\n\u002F* ---------- status \u002F output \u002F log ---------- *\u002F\n.status {\n  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;\n  font-size: 0.9rem; font-weight: 600;\n  padding: 11px 14px; border-radius: var(--radius-sm);\n  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink);\n}\n.status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n.status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n.status.bad  { border-color: color-mix(in srgb, var(--bad) 55%, var(--line)); }\n.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }\n.dot.good { background: var(--good); } .dot.warn { background: var(--warn); }\n.dot.bad { background: var(--bad); } .dot.info { background: var(--info); }\n\n.output { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; }\n.log { font-family: var(--mono); font-size: 0.8rem; line-height: 1.7; background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 11px 14px;\n  max-height: 220px; overflow-y: auto; white-space: pre-wrap; }\n\n\u002F* ---------- code block + syntax tokens ---------- *\u002F\n.code {\n  font-family: var(--mono); font-size: 0.82rem; line-height: 1.7;\n  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; overflow-x: auto; white-space: pre; tab-size: 2;\n}\n.code .tok-sel  { color: var(--accent); }\n.code .tok-prop { color: #86e29b; }\n.code .tok-kw   { color: #ff9bcb; }\n.code .tok-str  { color: #f0c674; }\n.code .tok-num  { color: #f0883e; }\n.code .tok-cmt  { color: var(--faint); font-style: italic; }\n.code .hl { background: color-mix(in srgb, var(--good) 22%, transparent); border-radius: 4px; padding: 0 3px; }\n\n\u002F* ---------- hint \u002F caption ---------- *\u002F\n.hint { font-size: 0.83rem; color: var(--muted); line-height: 1.55; margin: 10px 0 0; }\n.hint code { font-family: var(--mono); color: var(--ink); background: var(--line-soft);\n  padding: 1px 5px; border-radius: 5px; font-size: 0.86em; }\n.hint strong { color: var(--ink); }\n\n\u002F* ---------- banner (feature-support \u002F notices) ---------- *\u002F\n.banner {\n  display: none; margin-bottom: 14px; padding: 11px 14px; border-radius: 11px;\n  font-size: 0.85rem; line-height: 1.5;\n  color: var(--ink); background: color-mix(in srgb, var(--warn) 12%, var(--panel));\n  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--line));\n}\n.banner.show { display: block; }\n.banner.info { background: color-mix(in srgb, var(--info) 12%, var(--panel));\n  border-color: color-mix(in srgb, var(--info) 45%, var(--line)); }\n.banner code { font-family: var(--mono); background: var(--line-soft); padding: 1px 5px; border-radius: 5px; }\n\n\u002F* ---------- misc atoms ---------- *\u002F\n.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 700;\n  padding: 3px 9px; border-radius: 999px; background: var(--line-soft); color: var(--muted); }\n.pill.good { color: var(--good); background: color-mix(in srgb, var(--good) 16%, transparent); }\n.pill.bad  { color: var(--bad);  background: color-mix(in srgb, var(--bad) 16%, transparent); }\n.mono { font-family: var(--mono); }\n.muted { color: var(--muted); }\n.big-num { font-family: var(--mono); font-size: 1.6rem; font-weight: 800; letter-spacing: -0.02em; }\n\n\u002F* Optional theme toggle button (top-right). Wire it in JS if you want it. *\u002F\n.theme-btn { position: fixed; top: 12px; right: 12px; z-index: 20; }\n\n\u002F* ---------- responsive ---------- *\u002F\n@media (max-width: 560px) {\n  .panel { padding: 14px; }\n  .wrap-controls { gap: 14px; }\n  .btn { padding: 9px 13px; }\n}\n\n\u002F* ---------- reduced motion ---------- *\u002F\n@media (prefers-reduced-motion: reduce) {\n  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }\n}\n\n\n\u002F* The heartbeat: driven by requestAnimationFrame, so it stalls the instant the\n     main thread is blocked. (A pure CSS animation would be a lie here — browsers\n     can run those off the main thread.) *\u002F\n  .track {\n    position: relative; height: 44px; margin-top: 4px;\n    border: 1px dashed var(--line); border-radius: var(--radius-sm);\n    background: var(--panel-2); overflow: hidden;\n  }\n  .runner {\n    position: absolute; top: 50%; left: 0; width: 22px; height: 22px;\n    margin-top: -11px; border-radius: 50%;\n    background: var(--accent);\n    will-change: transform;\n  }\n  .track-label {\n    position: absolute; right: 8px; bottom: 4px;\n    font-family: var(--mono); font-size: .68rem; color: var(--muted);\n  }\n\n  .metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 12px; }\n  @media (max-width: 560px) { .metrics { grid-template-columns: 1fr; } }\n  .metric {\n    background: var(--panel-2); border: 1px solid var(--line);\n    border-radius: var(--radius-sm); padding: 10px 12px;\n  }\n  .metric .k { font-family: var(--mono); font-size: .68rem; color: var(--muted); display: block; }\n  .metric .v { font-size: 1.35rem; font-weight: 800; line-height: 1.25; }\n  .metric .v.good { color: #46d18b; }\n  .metric .v.bad  { color: #ff6b6b; }\n\n  table.results { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: .78rem; }\n  table.results th, table.results td {\n    text-align: right; padding: 7px 8px; border-bottom: 1px solid var(--line);\n    font-family: var(--mono);\n  }\n  table.results th:first-child, table.results td:first-child { text-align: left; }\n  table.results th { color: var(--muted); font-weight: 600; font-size: .7rem; }\n  table.results td.mode { font-family: inherit; font-weight: 700; }\n  table.results tr.pending td { color: var(--muted); opacity: .55; }\n\n  .clicker { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }\n  .clicker .count { font-family: var(--mono); font-size: .8rem; color: var(--muted); }","\"use strict\";\n  const $ = (id) => document.getElementById(id);\n\n  \u002F* ---------- the polyfill, written the way the post recommends ----------\n     `globalThis.scheduler` — NOT a bare `scheduler`. Optional chaining guards\n     against null\u002Fundefined values, not against an identifier that was never\n     declared, so `scheduler?.yield` throws a ReferenceError in a browser with no\n     scheduler global (Safari, today). *\u002F\n  const nativeYield = globalThis.scheduler?.yield?.bind(globalThis.scheduler);\n  const yieldToMain = nativeYield ?? (() => new Promise((r) => setTimeout(r, 0)));\n  const nextTimeout = () => new Promise((r) => setTimeout(r, 0));\n\n  $(\"support\").innerHTML = nativeYield\n    ? \"\u003Cstrong>Native \u003Ccode>scheduler.yield()\u003C\u002Fcode> detected.\u003C\u002Fstrong> The third button uses the real thing.\"\n    : \"\u003Cstrong>No native \u003Ccode>scheduler.yield()\u003C\u002Fcode> here.\u003C\u002Fstrong> The third button falls back to \" +\n      \"\u003Ccode>setTimeout(0)\u003C\u002Fcode>, so it will score much like the second one. Try this in Chrome&nbsp;129+ \" +\n      \"or Firefox&nbsp;142+ to see the difference.\";\n\n  \u002F* ---------- a calibrated unit of CPU work ---------- *\u002F\n  let itersPerMs = 20000; \u002F\u002F replaced by calibrate()\n  function burn(iters) {\n    let x = 1;\n    for (let i = 0; i \u003C iters; i++) x = (x * 1103515245 + 12345) % 2147483647;\n    return x;\n  }\n  function calibrate() {\n    const t0 = performance.now();\n    burn(400000);\n    const dt = performance.now() - t0;\n    if (dt > 0.5) itersPerMs = Math.max(1000, Math.round(400000 \u002F dt));\n  }\n\n  \u002F* ---------- the heartbeat: rAF frames + longest gap ---------- *\u002F\n  let frames = 0, lastTick = 0, worstGap = 0, tracking = false, phase = 0;\n  function tick() {\n    \u002F\u002F Measure on the real clock, sampled at callback entry — deliberately NOT the\n    \u002F\u002F timestamp rAF passes in. That argument is a frame clock, and in some\n    \u002F\u002F environments it is synthetic: headless Chrome reports a nominal 17 ms gap\n    \u002F\u002F across a thread that was provably blocked for 600 ms, which would turn the\n    \u002F\u002F headline \"longest freeze\" number into a fiction.\n    const now = performance.now();\n    if (lastTick) {\n      const gap = now - lastTick;\n      if (tracking) { frames++; if (gap > worstGap) worstGap = gap; }\n      phase = (phase + gap * 0.12) % 100;\n    }\n    lastTick = now;\n    const track = $(\"runner\").parentElement.clientWidth - 22;\n    $(\"runner\").style.transform = \"translateX(\" + ((phase \u002F 100) * track).toFixed(1) + \"px)\";\n    $(\"fps\").textContent = tracking ? frames + \" frames\" : \"idle\";\n    requestAnimationFrame(tick);\n  }\n  requestAnimationFrame(tick);\n\n  \u002F* ---------- click responsiveness ---------- *\u002F\n  let clicks = 0, worstClick = 0, pendingClick = 0;\n  $(\"clickMe\").addEventListener(\"pointerdown\", () => { pendingClick = performance.now(); });\n  $(\"clickMe\").addEventListener(\"click\", () => {\n    clicks++;\n    if (pendingClick) {\n      const delay = performance.now() - pendingClick;\n      if (delay > worstClick) worstClick = delay;\n      pendingClick = 0;\n    }\n    $(\"clickCount\").textContent =\n      clicks + \" click\" + (clicks === 1 ? \"\" : \"s\") + \" · worst delay \" +\n      (worstClick ? Math.round(worstClick) + \" ms\" : \"—\");\n  });\n\n  \u002F* ---------- source readout ---------- *\u002F\n  const SNIPPETS = {\n    block: [\n      ['\u003Cspan class=\"tok-kw\">function\u003C\u002Fspan> process(items) {', 0],\n      ['  \u003Cspan class=\"tok-kw\">for\u003C\u002Fspan> (\u003Cspan class=\"tok-kw\">const\u003C\u002Fspan> item \u003Cspan class=\"tok-kw\">of\u003C\u002Fspan> items) {', 0],\n      ['    processItem(item);', 0],\n      ['  }', 0],\n      ['\u003Cspan class=\"tok-cmt\">\u002F\u002F nothing yields — the thread is ours until we are done\u003C\u002Fspan>', 1],\n      ['}', 0],\n    ],\n    timeout: [\n      ['\u003Cspan class=\"tok-kw\">async function\u003C\u002Fspan> process(items) {', 0],\n      ['  \u003Cspan class=\"tok-kw\">let\u003C\u002Fspan> last = performance.\u003Cspan class=\"tok-prop\">now\u003C\u002Fspan>();', 0],\n      ['  \u003Cspan class=\"tok-kw\">for\u003C\u002Fspan> (\u003Cspan class=\"tok-kw\">const\u003C\u002Fspan> item \u003Cspan class=\"tok-kw\">of\u003C\u002Fspan> items) {', 0],\n      ['    processItem(item);', 0],\n      ['    \u003Cspan class=\"tok-kw\">if\u003C\u002Fspan> (performance.\u003Cspan class=\"tok-prop\">now\u003C\u002Fspan>() - last &gt; BUDGET) {', 0],\n      ['      \u003Cspan class=\"tok-kw\">await new\u003C\u002Fspan> \u003Cspan class=\"tok-prop\">Promise\u003C\u002Fspan>(r =&gt; \u003Cspan class=\"tok-prop\">setTimeout\u003C\u002Fspan>(r, \u003Cspan class=\"tok-num\">0\u003C\u002Fspan>));', 1],\n      ['      last = performance.\u003Cspan class=\"tok-prop\">now\u003C\u002Fspan>();', 0],\n      ['    }', 0],\n      ['  }', 0],\n      ['}', 0],\n    ],\n    yield: [\n      ['\u003Cspan class=\"tok-kw\">async function\u003C\u002Fspan> process(items) {', 0],\n      ['  \u003Cspan class=\"tok-kw\">let\u003C\u002Fspan> last = performance.\u003Cspan class=\"tok-prop\">now\u003C\u002Fspan>();', 0],\n      ['  \u003Cspan class=\"tok-kw\">for\u003C\u002Fspan> (\u003Cspan class=\"tok-kw\">const\u003C\u002Fspan> item \u003Cspan class=\"tok-kw\">of\u003C\u002Fspan> items) {', 0],\n      ['    processItem(item);', 0],\n      ['    \u003Cspan class=\"tok-kw\">if\u003C\u002Fspan> (performance.\u003Cspan class=\"tok-prop\">now\u003C\u002Fspan>() - last &gt; BUDGET) {', 0],\n      ['      \u003Cspan class=\"tok-kw\">await\u003C\u002Fspan> scheduler.\u003Cspan class=\"tok-prop\">yield\u003C\u002Fspan>();', 1],\n      ['      last = performance.\u003Cspan class=\"tok-prop\">now\u003C\u002Fspan>();', 0],\n      ['    }', 0],\n      ['  }', 0],\n      ['}', 0],\n    ],\n  };\n  function showCode(mode) {\n    $(\"code\").innerHTML = SNIPPETS[mode]\n      .map(([line, hot]) => (hot ? '\u003Cspan class=\"hl\">' + line + \"\u003C\u002Fspan>\" : line))\n      .join(\"\u003Cbr>\");\n  }\n\n  \u002F* ---------- results table ---------- *\u002F\n  const MODES = [\n    { key: \"block\",   label: \"Block the thread\" },\n    { key: \"timeout\", label: \"setTimeout(0)\" },\n    { key: \"yield\",   label: nativeYield ? \"scheduler.yield()\" : \"scheduler.yield() (fallback)\" },\n  ];\n  const results = {};\n  function renderTable() {\n    $(\"tbody\").innerHTML = MODES.map((m) => {\n      const r = results[m.key];\n      if (!r) return '\u003Ctr class=\"pending\">\u003Ctd class=\"mode\">' + m.label + \"\u003C\u002Ftd>\u003Ctd>—\u003C\u002Ftd>\u003Ctd>—\u003C\u002Ftd>\u003Ctd>—\u003C\u002Ftd>\u003Ctd>—\u003C\u002Ftd>\u003C\u002Ftr>\";\n      return \"\u003Ctr>\u003Ctd class=\\\"mode\\\">\" + m.label + \"\u003C\u002Ftd>\" +\n             \"\u003Ctd>\" + r.frames + \"\u003C\u002Ftd>\" +\n             \"\u003Ctd>\" + Math.round(r.worstGap) + \" ms\u003C\u002Ftd>\" +\n             \"\u003Ctd>\" + r.clicks + \"\u003C\u002Ftd>\" +\n             \"\u003Ctd>\" + Math.round(r.total) + \" ms\u003C\u002Ftd>\u003C\u002Ftr>\";\n    }).join(\"\");\n  }\n\n  function setStatus(text, kind) {\n    $(\"statusText\").textContent = text;\n    $(\"dot\").className = \"dot \" + kind;\n    $(\"status\").className = \"status \" + kind;\n  }\n  function setButtons(disabled) {\n    for (const id of [\"runBlock\", \"runTimeout\", \"runYield\"]) $(id).disabled = disabled;\n  }\n\n  \u002F* ---------- the run ---------- *\u002F\n  async function run(mode) {\n    const totalMs = +$(\"load\").value;\n    const budget = +$(\"budget\").value;\n    const ITEM_MS = 2;                                  \u002F\u002F each item ≈ 2 ms of CPU\n    const items = Math.max(1, Math.round(totalMs \u002F ITEM_MS));\n    const iters = itersPerMs * ITEM_MS;\n\n    showCode(mode);\n    setButtons(true);\n    frames = 0; worstGap = 0; clicks = 0; worstClick = 0;\n    $(\"clickCount\").textContent = \"0 clicks · worst delay —\";\n    setStatus(\"Running \" + items + \" items…\", \"warn\");\n\n    \u002F\u002F Let the status paint before we start hogging the thread.\n    await new Promise((r) => requestAnimationFrame(() => r()));\n    await new Promise((r) => setTimeout(r, 0));\n\n    tracking = true;\n    const t0 = performance.now();\n    let last = t0;\n\n    for (let i = 0; i \u003C items; i++) {\n      burn(iters);\n      if (mode !== \"block\" && performance.now() - last > budget) {\n        await (mode === \"yield\" ? yieldToMain() : nextTimeout());\n        last = performance.now();\n      }\n    }\n\n    const total = performance.now() - t0;\n\n    \u002F\u002F Keep measuring for exactly one more frame. During a fully blocked run no\n    \u002F\u002F rAF callback fires between start and finish, so the gap that spans the\n    \u002F\u002F freeze is only observable on the first frame AFTER the work ends. Stop\n    \u002F\u002F tracking before that frame lands and the headline number reads a bogus 0 ms.\n    await new Promise((r) => requestAnimationFrame(() => r()));\n    tracking = false;\n\n    results[mode] = { frames, worstGap, clicks, total };\n    renderTable();\n\n    $(\"mFrames\").textContent = frames;\n    $(\"mFrames\").className = \"v \" + (frames > 10 ? \"good\" : \"bad\");\n    $(\"mFreeze\").textContent = Math.round(worstGap) + \" ms\";\n    $(\"mFreeze\").className = \"v \" + (worstGap > 100 ? \"bad\" : \"good\");\n    $(\"mTotal\").textContent = Math.round(total) + \" ms\";\n    $(\"mTotal\").className = \"v\";\n\n    if (mode === \"block\") {\n      setStatus(\"Blocked for \" + Math.round(worstGap) + \" ms — the browser painted \" + frames + \" frame(s).\", \"bad\");\n      $(\"hint\").innerHTML =\n        \"That single freeze is one long task. Every click, keypress and paint queued behind it. \" +\n        \"If you clicked during the run, look at the worst delay above — that is the lag a user feels.\";\n    } else {\n      const nice = worstGap \u003C 120;\n      setStatus(\n        \"Painted \" + frames + \" frames, worst freeze \" + Math.round(worstGap) + \" ms — the UI stayed \" +\n          (nice ? \"responsive\" : \"mostly responsive\") + \".\",\n        nice ? \"good\" : \"warn\"\n      );\n      $(\"hint\").innerHTML =\n        \"Same arithmetic, same total work — but the browser got turns in between. Lower the yield \" +\n        \"budget and the worst freeze drops further; raise it and you drift back toward blocking. \" +\n        \"Around \u003Cb>50&nbsp;ms\u003C\u002Fb> is the sweet spot, which is why that number is in every example.\";\n    }\n    setButtons(false);\n  }\n\n  $(\"runBlock\").addEventListener(\"click\", () => run(\"block\"));\n  $(\"runTimeout\").addEventListener(\"click\", () => run(\"timeout\"));\n  $(\"runYield\").addEventListener(\"click\", () => run(\"yield\"));\n\n  $(\"load\").addEventListener(\"input\", () => { $(\"loadOut\").textContent = $(\"load\").value + \" ms\"; });\n  $(\"budget\").addEventListener(\"input\", () => { $(\"budgetOut\").textContent = $(\"budget\").value + \" ms\"; });\n\n  calibrate();\n  showCode(\"yield\");\n  renderTable();",{"height":40,"autorun":17},520,{"slug":33,"title":42,"excerpt":43,"coverUrl":44},"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"]