[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-indexeddb-localstorage-main-thread-blocking":44,"search-suggestions":58},[4,20,32],{"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","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","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.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"slug":45,"title":46,"summary":47,"html":48,"css":49,"js":50,"config":51,"doc":53},"indexeddb-localstorage-main-thread-blocking","localStorage vs IndexedDB — watch the main thread freeze",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🎮 Does the animation freeze?\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      The blue dot slides back and forth on every animation frame — as long as the main\n      thread is free to run it. Pick a payload size, then fire it at\n      \u003Ccode>localStorage.setItem\u003C\u002Fcode> (synchronous) or \u003Ccode>IndexedDB\u003C\u002Fcode> (asynchronous)\n      and watch what happens to the dot, not just the numbers.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Pick a payload size\u003C\u002Fdiv>\n      \u003Cdiv class=\"presets\" id=\"presets\">\n        \u003Cbutton class=\"btn sm\" data-size=\"0.1\" aria-pressed=\"false\">100 KB\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn sm\" data-size=\"1\" aria-pressed=\"true\">1 MB\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn sm\" data-size=\"3\" aria-pressed=\"false\">3 MB\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn sm\" data-size=\"6\" aria-pressed=\"false\">6 MB\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\">Bigger payload → more work for \u003Ccode>JSON.stringify\u003C\u002Fcode> and the write itself. 6 MB may even hit some browsers' storage quota — that's the point.\u003C\u002Fp>\n      \u003Cdiv class=\"btn-row\" style=\"margin-top:14px\">\n        \u003Cbutton class=\"btn bad\" id=\"runLocal\">Write with localStorage (sync)\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn good\" id=\"runIdb\">Write with IndexedDB (async)\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn ghost sm\" id=\"clear\">Clear storage\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> Watch the dot\u003C\u002Fdiv>\n      \u003Cdiv class=\"stage\" id=\"stage\">\u003Cdiv class=\"runner\" id=\"runner\">\u003C\u002Fdiv>\u003C\u002Fdiv>\n      \u003Cdiv class=\"stage-caption\">\n        \u003Cspan>Smooth = main thread free. Frozen = main thread blocked.\u003C\u002Fspan>\n        \u003Cspan id=\"fps\">— fps\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"metrics\">\n        \u003Cdiv class=\"metric\">\n          \u003Cdiv class=\"k\">setItem() call took\u003C\u002Fdiv>\n          \u003Cdiv class=\"v\" id=\"mCall\">—\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv class=\"metric\">\n          \u003Cdiv class=\"k\">Longest frame gap\u003C\u002Fdiv>\n          \u003Cdiv class=\"v\" id=\"mGap\">—\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv class=\"metric\">\n          \u003Cdiv class=\"k\">Dropped frames\u003C\u002Fdiv>\n          \u003Cdiv class=\"v\" id=\"mDrop\">—\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"status\" style=\"margin-top:12px\">\n        \u003Cspan class=\"dot info\" id=\"dotStatus\">\u003C\u002Fspan>\u003Cspan id=\"statusText\">Ready — pick a size, then write.\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"hint\">\n        \u003Ccode>localStorage.setItem\u003C\u002Fcode> is spec'd as synchronous: nothing else on this\n        thread — including the animation frame that moves the dot — runs until it returns.\n        \u003Ccode>IndexedDB\u003C\u002Fcode> hands back a request immediately; the actual write happens\n        off the call stack, so the dot keeps sliding.\n      \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.stage {\n    position: relative;\n    height: 64px;\n    border: 1px dashed var(--line);\n    border-radius: var(--radius-sm);\n    background: var(--panel-2);\n    overflow: hidden;\n  }\n  .runner {\n    position: absolute;\n    top: 50%;\n    left: 0;\n    width: 20px;\n    height: 20px;\n    margin-top: -10px;\n    border-radius: 50%;\n    background: var(--accent);\n    box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 25%, transparent);\n  }\n  .stage-caption {\n    display: flex;\n    justify-content: space-between;\n    font-size: 0.78rem;\n    color: var(--muted);\n    margin-top: 8px;\n  }\n  .metrics {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    gap: 10px;\n    margin-top: 12px;\n  }\n  @media (max-width: 640px) { .metrics { grid-template-columns: 1fr; } }\n  .metric {\n    background: var(--card);\n    border: 1px solid var(--line);\n    border-radius: var(--radius-sm);\n    padding: 10px 12px;\n  }\n  .metric .k { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }\n  .metric .v { font-family: var(--mono); font-size: 1.05rem; margin-top: 3px; }\n  .metric .v.bad { color: var(--bad); }\n  .metric .v.good { color: var(--good); }","\"use strict\";\n  const $ = (id) => document.getElementById(id);\n\n  let sizeMB = 1;\n  document.querySelectorAll(\"#presets .btn\").forEach((btn) => {\n    btn.addEventListener(\"click\", () => {\n      document.querySelectorAll(\"#presets .btn\").forEach((b) => b.setAttribute(\"aria-pressed\", \"false\"));\n      btn.setAttribute(\"aria-pressed\", \"true\");\n      sizeMB = parseFloat(btn.dataset.size);\n    });\n  });\n\n  function payload(mb) {\n    const chars = Math.round(mb * 1024 * 1024);\n    return { text: \"x\".repeat(chars), savedAt: Date.now() };\n  }\n\n  function setStatus(text, kind = \"info\") {\n    $(\"statusText\").textContent = text;\n    $(\"dotStatus\").className = \"dot \" + kind;\n  }\n\n  \u002F\u002F --- the moving dot, driven by requestAnimationFrame ---\n  const runner = $(\"runner\");\n  const stageWidth = () => $(\"stage\").clientWidth - 20;\n  let raf = null;\n  let frameCount = 0;\n  let lastSecond = performance.now();\n  let lastFrame = performance.now();\n  let longestGap = 0;\n  let droppedFrames = 0; \u002F\u002F frames further apart than ~50ms (3x a 60fps frame)\n\n  function tick(now) {\n    const gap = now - lastFrame;\n    if (gap > longestGap) longestGap = gap;\n    if (gap > 50) droppedFrames++;\n    lastFrame = now;\n\n    const t = (now \u002F 1400) % 2; \u002F\u002F 0..2 triangle wave\n    const frac = t \u003C 1 ? t : 2 - t;\n    runner.style.left = (frac * stageWidth()) + \"px\";\n\n    frameCount++;\n    if (now - lastSecond >= 1000) {\n      $(\"fps\").textContent = frameCount + \" fps\";\n      frameCount = 0;\n      lastSecond = now;\n    }\n    raf = requestAnimationFrame(tick);\n  }\n  raf = requestAnimationFrame(tick);\n\n  function resetMetrics() {\n    longestGap = 0;\n    droppedFrames = 0;\n    $(\"mGap\").textContent = \"—\";\n    $(\"mDrop\").textContent = \"—\";\n  }\n\n  function openDb() {\n    return new Promise((resolve, reject) => {\n      const req = indexedDB.open(\"pg-storage-demo\", 1);\n      req.onupgradeneeded = () => req.result.createObjectStore(\"kv\");\n      req.onsuccess = () => resolve(req.result);\n      req.onerror = () => reject(req.error);\n    });\n  }\n\n  $(\"runLocal\").addEventListener(\"click\", () => {\n    resetMetrics();\n    setStatus(`Writing ${sizeMB} MB synchronously — watch the dot...`, \"warn\");\n    const data = JSON.stringify(payload(sizeMB));\n    \u002F\u002F Give the browser one frame to render the \"writing...\" status before we block it.\n    requestAnimationFrame(() => {\n      const t0 = performance.now();\n      try {\n        localStorage.setItem(\"pg-demo\", data);\n        const t1 = performance.now();\n        $(\"mCall\").textContent = (t1 - t0).toFixed(1) + \" ms\";\n        $(\"mCall\").className = \"v bad\";\n        setTimeout(() => {\n          $(\"mGap\").textContent = longestGap.toFixed(0) + \" ms\";\n          $(\"mDrop\").textContent = String(droppedFrames);\n          setStatus(`Blocked the main thread for ${(t1 - t0).toFixed(1)} ms — that's why the dot stuttered.`, \"bad\");\n        }, 400);\n      } catch (err) {\n        $(\"mCall\").textContent = \"QuotaExceededError\";\n        $(\"mCall\").className = \"v bad\";\n        setStatus(\"localStorage rejected the write — quota exceeded. That's the ~5MB wall.\", \"bad\");\n      }\n    });\n  });\n\n  $(\"runIdb\").addEventListener(\"click\", async () => {\n    resetMetrics();\n    setStatus(`Writing ${sizeMB} MB with IndexedDB — dot should keep sliding...`, \"info\");\n    const data = payload(sizeMB);\n    try {\n      const db = await openDb();\n      const t0 = performance.now();\n      const tx = db.transaction(\"kv\", \"readwrite\");\n      tx.objectStore(\"kv\").put(data, \"pg-demo\");\n      const tCallReturned = performance.now();\n      $(\"mCall\").textContent = (tCallReturned - t0).toFixed(2) + \" ms (call returns)\";\n      $(\"mCall\").className = \"v good\";\n      tx.oncomplete = () => {\n        const t1 = performance.now();\n        setTimeout(() => {\n          $(\"mGap\").textContent = longestGap.toFixed(0) + \" ms\";\n          $(\"mDrop\").textContent = String(droppedFrames);\n          setStatus(`put() returned in ${(tCallReturned - t0).toFixed(2)} ms; write finished ${(t1 - t0).toFixed(0)} ms later in the background — the dot never stopped.`, \"good\");\n        }, 400);\n      };\n      tx.onerror = () => setStatus(\"IndexedDB transaction failed: \" + tx.error, \"bad\");\n    } catch (err) {\n      setStatus(\"IndexedDB isn't available in this context: \" + err.message, \"bad\");\n    }\n  });\n\n  $(\"clear\").addEventListener(\"click\", async () => {\n    try { localStorage.removeItem(\"pg-demo\"); } catch {}\n    try {\n      const db = await openDb();\n      db.transaction(\"kv\", \"readwrite\").objectStore(\"kv\").delete(\"pg-demo\");\n    } catch {}\n    resetMetrics();\n    $(\"mCall\").textContent = \"—\";\n    $(\"mCall\").className = \"v\";\n    setStatus(\"Cleared. Ready — pick a size, then write.\", \"info\");\n  });",{"height":52,"autorun":17},520,{"slug":45,"title":54,"excerpt":55,"coverUrl":56,"vertical":57},"localStorage Isn't Free — It's Blocking Your Main Thread","Autosave into localStorage looks harmless right up until the draft gets big. The API is synchronous by spec, so every write freezes the page for exactly as long as it takes — and debouncing doesn't fix it. IndexedDB does.","\u002Fmedia\u002Fcovers\u002Findexeddb-localstorage-main-thread-blocking.png",{"slug":6},[59,63,67,71,75,79,83,87,91,95,98,102],{"slug":60,"name":61,"articles":62},"webdev","Webdev",111,{"slug":64,"name":65,"articles":66},"javascript","Javascript",93,{"slug":68,"name":69,"articles":70},"frontend","Frontend",73,{"slug":72,"name":73,"articles":74},"tutorial","Tutorial",39,{"slug":76,"name":77,"articles":78},"css","Css",36,{"slug":80,"name":81,"articles":82},"typescript","Typescript",16,{"slug":84,"name":85,"articles":86},"performance","Performance",14,{"slug":88,"name":89,"articles":90},"react","React",13,{"slug":92,"name":93,"articles":94},"browser","Browser",10,{"slug":96,"name":97,"articles":94},"node","Node",{"slug":99,"name":100,"articles":101},"html","Html",8,{"slug":103,"name":104,"articles":105},"accessibility","Accessibility",7]