[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-indexeddb-localstorage-main-thread-blocking":44,"search-suggestions":60,"quiz-article-indexeddb-localstorage-main-thread-blocking":107},[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,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a0ddae-4592-71eb-83f7-e14ac53bac6a","indexeddb-localstorage-main-thread-blocking","PRACTICE_QUIZ","localStorage vs IndexedDB — the main-thread cost","Eight questions on why localStorage.setItem blocks the page, why debouncing doesn't fix it, and how IndexedDB avoids the freeze.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,103],{"slug":62,"name":63,"articles":64},"webdev","Webdev",111,{"slug":66,"name":67,"articles":68},"javascript","Javascript",93,{"slug":70,"name":71,"articles":72},"frontend","Frontend",73,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",39,{"slug":78,"name":79,"articles":80},"css","Css",36,{"slug":82,"name":83,"articles":84},"typescript","Typescript",16,{"slug":86,"name":87,"articles":88},"performance","Performance",14,{"slug":90,"name":91,"articles":92},"react","React",13,{"slug":94,"name":95,"articles":96},"browser","Browser",10,{"slug":98,"name":99,"articles":96},"node","Node",{"slug":101,"name":102,"articles":51},"html","Html",{"slug":104,"name":105,"articles":106},"accessibility","Accessibility",7,{"id":108,"slug":46,"title":109,"subtitle":52,"excerpt":110,"coverUrl":111,"locale":13,"readingMinutes":112,"publishedAt":113,"viewCount":114,"likeCount":19,"commentCount":19,"author":115,"vertical":120,"topic":121,"tags":123,"_count":128,"playground":130,"body":132,"bodyMd":252,"seo":253,"translationGroupId":256,"series":52,"podcastUrl":52,"verticalId":5,"thread":257,"assessments":259,"translations":262,"quiz":264},"01a0ddae-453b-76e9-8652-04984e4978ad","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",5,"2026-09-26T12:25:07.879Z",50,{"id":116,"name":117,"username":118,"avatarUrl":52,"headline":119},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":66,"name":122},"JavaScript",[124,125,126,127],{"slug":66,"name":67,"color":52},{"slug":62,"name":63,"color":52},{"slug":86,"name":87,"color":52},{"slug":74,"name":75,"color":52},{"assessments":129},1,{"slug":46,"title":131},"localStorage vs IndexedDB — watch the main thread freeze",{"blocks":133,"version":129},[134,138,141,144,149,152,158,161,164,168,171,174,177,181,184,188,191,194,197,200,203,206,209,212,215,218,221,224,227,230,233,236,239,242,245],{"id":135,"html":136,"type":137},"b1","\u003Cp>You add autosave to a text editor. Every few seconds, whatever&#39;s in the textarea gets written to \u003Ccode>localStorage\u003C\u002Fcode> so a refresh — or a crashed tab — doesn&#39;t cost the user their draft. You test it with a paragraph. Smooth. Ship it.\u003C\u002Fp>","paragraph",{"id":139,"html":140,"type":137},"b2","\u003Cp>Then someone pastes in a 6,000-word draft, and every autosave tick makes the whole page hitch for a beat — the cursor stalls, the animation in the sidebar stutters, keystrokes queue up and land late. Nothing crashed. No error. The console is clean. It just... pauses. Repeatedly. Forever, as long as the draft stays that big.\u003C\u002Fp>",{"id":142,"html":143,"type":137},"b3","\u003Cp>\u003Cstrong>Guess before you scroll:\u003C\u002Fstrong> it&#39;s not a bug in your debounce logic. The debounce is fine. The freeze is coming from a function you&#39;d swear is instant, because it always has been — right up until the argument got large.\u003C\u002Fp>",{"id":145,"html":146,"text":147,"type":148,"level":31},"b4","The obvious fix that doesn&#39;t fix it","The obvious fix that doesn't fix it","heading",{"id":150,"html":151,"type":137},"b5","\u003Cp>Your first move is reasonable: you&#39;re calling \u003Ccode>localStorage.setItem\u003C\u002Fcode> too often, so throttle it.\u003C\u002Fp>",{"id":153,"code":154,"type":155,"language":156,"highlight":157},"b6","const saveDraft = debounce((text) => {\n  localStorage.setItem(\"draft\", JSON.stringify({ text, savedAt: Date.now() }));\n}, 2000);\n\ntextarea.addEventListener(\"input\", (e) => saveDraft(e.target.value));","code","js",[],{"id":159,"html":160,"type":137},"b7","\u003Cp>Autosaves now fire every two seconds instead of every keystroke. On a small draft, the stutter is gone — but that&#39;s because small drafts were never the problem. Paste the same 6,000-word draft back in and the page still hitches, just less often. Every two seconds instead of every keystroke, but each hitch is exactly as long as before.\u003C\u002Fp>",{"id":162,"html":163,"type":137},"b8","\u003Cp>That&#39;s the tell. Debouncing controls \u003Cem>frequency\u003C\u002Fem>. It does nothing to the \u003Cem>duration\u003C\u002Fem> of a single call. If one call to \u003Ccode>setItem\u003C\u002Fcode> blocks for 40ms, calling it less often gives you fewer 40ms freezes — not shorter ones.\u003C\u002Fp>",{"id":165,"html":166,"text":167,"type":148,"level":31},"b9","What&#39;s actually blocking","What's actually blocking",{"id":169,"html":170,"type":137},"b10","\u003Cp>\u003Ccode>localStorage\u003C\u002Fcode> is a synchronous API. Not &quot;usually fast&quot; — synchronously specified. \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWindow\u002FlocalStorage\">MDN is direct about it\u003C\u002Fa>: every \u003Ccode>getItem\u003C\u002Fcode> and \u003Ccode>setItem\u003C\u002Fcode> call runs to completion on the thread that called it before anything else on that thread can run. No other event handler fires, no frame paints, no \u003Ccode>requestAnimationFrame\u003C\u002Fcode> callback executes, until that one line returns.\u003C\u002Fp>",{"id":172,"html":173,"type":137},"b11","\u003Cp>For a short string, &quot;runs to completion&quot; is sub-millisecond — you&#39;ll never see it. But your draft isn&#39;t a short string. Before it ever reaches \u003Ccode>setItem\u003C\u002Fcode>, it goes through \u003Ccode>JSON.stringify\u003C\u002Fcode> on a growing object, and then the storage write itself has to serialize and persist that string. Both steps happen on the same call, on the same thread, with the same guarantee: nothing else runs until it&#39;s done. A 5MB draft doesn&#39;t make \u003Ccode>setItem\u003C\u002Fcode> async — it just makes the synchronous part take longer, and the page is unresponsive for exactly that long, every single time you call it.\u003C\u002Fp>",{"id":175,"html":176,"type":137},"b12","\u003Cp>There&#39;s a second cost stacked on top: \u003Ccode>localStorage\u003C\u002Fcode> only stores strings. Every save round-trips your data through \u003Ccode>JSON.stringify\u003C\u002Fcode>, and every load round-trips it back through \u003Ccode>JSON.parse\u003C\u002Fcode> — both synchronous, both scaling with payload size, both adding to the freeze. And you&#39;re working inside a ceiling most browsers put at roughly 5MB per origin for all of \u003Ccode>localStorage\u003C\u002Fcode> combined, draft included. Get there and \u003Ccode>setItem\u003C\u002Fcode> throws \u003Ccode>QuotaExceededError\u003C\u002Fcode> instead of saving — which, for an autosave feature, is worse than a stutter.\u003C\u002Fp>",{"id":178,"html":179,"text":180,"type":148,"level":31},"b13","The fix: an API that doesn&#39;t block","The fix: an API that doesn't block",{"id":182,"html":183,"type":137},"b14","\u003Cp>\u003Ccode>IndexedDB\u003C\u002Fcode> solves the actual problem, not a symptom of it. It&#39;s asynchronous from the ground up: you open a transaction, call \u003Ccode>.put()\u003C\u002Fcode>, and get a \u003Ccode>request\u003C\u002Fcode> object back immediately. The real work — serializing and persisting the value — happens off the synchronous call stack. Your code (and the browser&#39;s renderer) keeps running while it does.\u003C\u002Fp>",{"id":185,"code":186,"type":155,"language":156,"highlight":187},"b15","function openDraftsDb() {\n  return new Promise((resolve, reject) => {\n    const req = indexedDB.open(\"editor\", 1);\n    req.onupgradeneeded = () => req.result.createObjectStore(\"drafts\");\n    req.onsuccess = () => resolve(req.result);\n    req.onerror = () => reject(req.error);\n  });\n}\n\nasync function saveDraft(text) {\n  const db = await openDraftsDb();\n  const tx = db.transaction(\"drafts\", \"readwrite\");\n  tx.objectStore(\"drafts\").put({ text, savedAt: Date.now() }, \"current\");\n  \u002F\u002F tx.oncomplete fires later — the call above already returned.\n}",[],{"id":189,"html":190,"type":137},"b16","\u003Cp>Notice what&#39;s missing: no \u003Ccode>JSON.stringify\u003C\u002Fcode>. \u003Ccode>IndexedDB\u003C\u002Fcode> uses the \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWeb_Workers_API\u002FStructured_clone_algorithm\">structured clone algorithm\u003C\u002Fa> instead of JSON serialization, so you can hand it the plain object — even \u003Ccode>Date\u003C\u002Fcode>s, \u003Ccode>Map\u003C\u002Fcode>s, \u003Ccode>Blob\u003C\u002Fcode>s — and it stores the value directly. And the storage ceiling isn&#39;t a fixed 5MB; it&#39;s a share of whatever disk space the browser is willing to grant the origin, which you can check with \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FStorageManager\u002Festimate\">\u003Ccode>navigator.storage.estimate()\u003C\u002Fcode>\u003C\u002Fa> and which is typically a large fraction of free disk space, not a hardcoded number.\u003C\u002Fp>",{"id":192,"html":193,"type":137},"b17","\u003Cp>The trade you&#39;re making is real, not free: \u003Ccode>IndexedDB\u003C\u002Fcode>&#39;s API is callback- and event-based and noticeably more ceremony than \u003Ccode>setItem(key, value)\u003C\u002Fcode>. For a handful of small, infrequent values — a theme preference, a feature flag — \u003Ccode>localStorage\u003C\u002Fcode>&#39;s synchronous simplicity is still the right call; the blocking never gets large enough to notice. The line to watch is payload size and write frequency, not &quot;is this data important.&quot; A dark-mode toggle can live in \u003Ccode>localStorage\u003C\u002Fcode> forever. A growing document, a cart with attachments, or anything you write on every keystroke should not.\u003C\u002Fp>",{"id":195,"html":196,"type":137},"b18","\u003C!-- playground:start -->",{"id":198,"html":199,"text":199,"type":148,"level":31},"b19","🎮 Try it yourself",{"id":201,"html":202,"type":137},"b20","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Findexeddb-localstorage-main-thread-blocking\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":204,"html":205,"type":137},"b21","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":207,"html":208,"type":137},"b22","\u003C!-- playground:end -->",{"id":210,"html":211,"type":137},"b23","\u003Cp>Numbers in a paragraph don&#39;t land the way a stuttering animation does. The playground runs a \u003Ccode>requestAnimationFrame\u003C\u002Fcode> loop — a dot sliding back and forth, smooth as long as the main thread is free — next to two buttons that write the same payload two different ways. One call blocks the loop. The other doesn&#39;t. Watch the dot, not the numbers.\u003C\u002Fp>",{"id":213,"html":214,"text":214,"type":148,"level":31},"b24","The lesson",{"id":216,"html":217,"type":137},"b25","\u003Cp>&quot;It&#39;s just a \u003Ccode>localStorage.setItem\u003C\u002Fcode> call&quot; is true and also exactly why it&#39;s easy to miss — the API looks identical whether the payload is 12 bytes or 12 megabytes, and only one of those is a problem. Debouncing a synchronous call gives you the same freeze, less often. Only an actually asynchronous API — \u003Ccode>IndexedDB\u003C\u002Fcode>, here — gives you a shorter one.\u003C\u002Fp>",{"id":219,"html":220,"type":137},"b26","\u003Cp>Go check what you&#39;re writing to \u003Ccode>localStorage\u003C\u002Fcode> on a hot path — an editor, a form draft, anything with attachments or growing text. If the payload can grow past a few dozen KB, that&#39;s worth five minutes today. What&#39;s the biggest thing you&#39;ve ever accidentally shoved into \u003Ccode>localStorage\u003C\u002Fcode>?\u003C\u002Fp>",{"id":222,"html":223,"type":137},"b27","\u003C!-- quiz:start -->",{"id":225,"html":226,"text":226,"type":148,"level":31},"b28","🧠 Test yourself",{"id":228,"html":229,"type":137},"b29","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Findexeddb-localstorage-main-thread-blocking\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":231,"html":232,"type":137},"b30","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":234,"html":235,"type":137},"b31","\u003C!-- quiz:end -->",{"id":237,"type":238},"b32","divider",{"id":240,"html":241,"type":137},"b33","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":243,"html":244,"type":137},"b34","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":246,"type":247,"items":248,"ordered":18},"b35","list",[249,250,251],"⭐ \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>","You add autosave to a text editor. Every few seconds, whatever's in the textarea gets written to `localStorage` so a refresh — or a crashed tab — doesn't cost the user their draft. You test it with a paragraph. Smooth. Ship it.\n\nThen someone pastes in a 6,000-word draft, and every autosave tick makes the whole page hitch for a beat — the cursor stalls, the animation in the sidebar stutters, keystrokes queue up and land late. Nothing crashed. No error. The console is clean. It just... pauses. Repeatedly. Forever, as long as the draft stays that big.\n\n**Guess before you scroll:** it's not a bug in your debounce logic. The debounce is fine. The freeze is coming from a function you'd swear is instant, because it always has been — right up until the argument got large.\n\n## The obvious fix that doesn't fix it\n\nYour first move is reasonable: you're calling `localStorage.setItem` too often, so throttle it.\n\n```js\nconst saveDraft = debounce((text) => {\n  localStorage.setItem(\"draft\", JSON.stringify({ text, savedAt: Date.now() }));\n}, 2000);\n\ntextarea.addEventListener(\"input\", (e) => saveDraft(e.target.value));\n```\n\nAutosaves now fire every two seconds instead of every keystroke. On a small draft, the stutter is gone — but that's because small drafts were never the problem. Paste the same 6,000-word draft back in and the page still hitches, just less often. Every two seconds instead of every keystroke, but each hitch is exactly as long as before.\n\nThat's the tell. Debouncing controls *frequency*. It does nothing to the *duration* of a single call. If one call to `setItem` blocks for 40ms, calling it less often gives you fewer 40ms freezes — not shorter ones.\n\n## What's actually blocking\n\n`localStorage` is a synchronous API. Not \"usually fast\" — synchronously specified. [MDN is direct about it](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWindow\u002FlocalStorage): every `getItem` and `setItem` call runs to completion on the thread that called it before anything else on that thread can run. No other event handler fires, no frame paints, no `requestAnimationFrame` callback executes, until that one line returns.\n\nFor a short string, \"runs to completion\" is sub-millisecond — you'll never see it. But your draft isn't a short string. Before it ever reaches `setItem`, it goes through `JSON.stringify` on a growing object, and then the storage write itself has to serialize and persist that string. Both steps happen on the same call, on the same thread, with the same guarantee: nothing else runs until it's done. A 5MB draft doesn't make `setItem` async — it just makes the synchronous part take longer, and the page is unresponsive for exactly that long, every single time you call it.\n\nThere's a second cost stacked on top: `localStorage` only stores strings. Every save round-trips your data through `JSON.stringify`, and every load round-trips it back through `JSON.parse` — both synchronous, both scaling with payload size, both adding to the freeze. And you're working inside a ceiling most browsers put at roughly 5MB per origin for all of `localStorage` combined, draft included. Get there and `setItem` throws `QuotaExceededError` instead of saving — which, for an autosave feature, is worse than a stutter.\n\n## The fix: an API that doesn't block\n\n`IndexedDB` solves the actual problem, not a symptom of it. It's asynchronous from the ground up: you open a transaction, call `.put()`, and get a `request` object back immediately. The real work — serializing and persisting the value — happens off the synchronous call stack. Your code (and the browser's renderer) keeps running while it does.\n\n```js\nfunction openDraftsDb() {\n  return new Promise((resolve, reject) => {\n    const req = indexedDB.open(\"editor\", 1);\n    req.onupgradeneeded = () => req.result.createObjectStore(\"drafts\");\n    req.onsuccess = () => resolve(req.result);\n    req.onerror = () => reject(req.error);\n  });\n}\n\nasync function saveDraft(text) {\n  const db = await openDraftsDb();\n  const tx = db.transaction(\"drafts\", \"readwrite\");\n  tx.objectStore(\"drafts\").put({ text, savedAt: Date.now() }, \"current\");\n  \u002F\u002F tx.oncomplete fires later — the call above already returned.\n}\n```\n\nNotice what's missing: no `JSON.stringify`. `IndexedDB` uses the [structured clone algorithm](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWeb_Workers_API\u002FStructured_clone_algorithm) instead of JSON serialization, so you can hand it the plain object — even `Date`s, `Map`s, `Blob`s — and it stores the value directly. And the storage ceiling isn't a fixed 5MB; it's a share of whatever disk space the browser is willing to grant the origin, which you can check with [`navigator.storage.estimate()`](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FStorageManager\u002Festimate) and which is typically a large fraction of free disk space, not a hardcoded number.\n\nThe trade you're making is real, not free: `IndexedDB`'s API is callback- and event-based and noticeably more ceremony than `setItem(key, value)`. For a handful of small, infrequent values — a theme preference, a feature flag — `localStorage`'s synchronous simplicity is still the right call; the blocking never gets large enough to notice. The line to watch is payload size and write frequency, not \"is this data important.\" A dark-mode toggle can live in `localStorage` forever. A growing document, a cart with attachments, or anything you write on every keystroke should not.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Findexeddb-localstorage-main-thread-blocking\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\nNumbers in a paragraph don't land the way a stuttering animation does. The playground runs a `requestAnimationFrame` loop — a dot sliding back and forth, smooth as long as the main thread is free — next to two buttons that write the same payload two different ways. One call blocks the loop. The other doesn't. Watch the dot, not the numbers.\n\n## The lesson\n\n\"It's just a `localStorage.setItem` call\" is true and also exactly why it's easy to miss — the API looks identical whether the payload is 12 bytes or 12 megabytes, and only one of those is a problem. Debouncing a synchronous call gives you the same freeze, less often. Only an actually asynchronous API — `IndexedDB`, here — gives you a shorter one.\n\nGo check what you're writing to `localStorage` on a hot path — an editor, a form draft, anything with attachments or growing text. If the payload can grow past a few dozen KB, that's worth five minutes today. What's the biggest thing you've ever accidentally shoved into `localStorage`?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Findexeddb-localstorage-main-thread-blocking\u002Fquiz)**\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---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\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)",{"title":109,"canonical":254,"description":255},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Findexeddb-localstorage-main-thread-blocking","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 d","01a0ddae-453b-76e9-8652-0b088c274c24",{"id":258,"locked":18},"01a0ddae-4566-708a-9bf3-1c6ed1aebda4",[260],{"id":45,"slug":46,"title":48,"_count":261},{"questions":51},[263],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":265,"questionCount":51},{"questions":51}]