Synchronous vs asynchronous storage
The main thread is JavaScript’s single execution lane
Section titled “The main thread is JavaScript’s single execution lane”Browsers run JavaScript, layout, and painting on a single main thread. When any JavaScript call takes a long time to return, it holds up everything else — animations stutter, clicks go unresponsive, and the browser may show a “page not responding” warning. This is called jank.
Synchronous storage: Web Storage and cookies
Section titled “Synchronous storage: Web Storage and cookies”localStorage, sessionStorage, and document.cookie are all synchronous. When you call localStorage.getItem('key'), the browser reads from disk, returns the value, and only then lets your code continue. Your script cannot do anything else while it waits.
For a single small read this is imperceptible. But problems surface when you:
- Loop over thousands of keys
- Store large serialised objects (many kilobytes each)
- Write on every keystroke or scroll event
- Parse the entire cookie string on a hot code path
Because all of this happens on the main thread, heavy Web Storage use can introduce measurable frame-time delays.
// Every one of these calls is synchronous — the thread blocks until each completesconst start = performance.now();for (let i = 0; i < 1000; i++) { localStorage.setItem('key-' + i, 'value-' + i);}const elapsed = performance.now() - start;console.log('1 000 writes took ' + elapsed.toFixed(2) + ' ms');Asynchronous storage: IndexedDB and Cache API
Section titled “Asynchronous storage: IndexedDB and Cache API”IndexedDB and the Cache API are asynchronous. Every operation returns a Promise (or fires an event) and your code continues immediately. The actual I/O happens off the main thread. When results are ready, the browser calls your callback or resolves your Promise.
// This returns immediately — the write happens in the backgroundconst db = await idb.openDB('my-db', 1, { upgrade(db) { db.createObjectStore('items'); }});await db.put('items', { name: 'Alice' }, 'user-1');console.log('Write complete — main thread was never blocked');Two-lane model
Section titled “Two-lane model”flowchart LR
subgraph Sync["Synchronous (Web Storage / Cookies)"]
direction TB
T1[Main thread] -->|blocked| R1[localStorage.getItem]
R1 -->|returns| T1
end
subgraph Async["Asynchronous (IndexedDB / Cache API)"]
direction TB
T2[Main thread] -->|fires request| IDB[IndexedDB / Cache]
T2 -->|continues immediately| T2
IDB -->|Promise resolves| T2
end Runnable timing comparison
Section titled “Runnable timing comparison”The snippet below times 10 000 localStorage.setItem calls synchronously, logs the elapsed time, and then cleans up every demo: key it created.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| Synchronous (Web Storage / cookies) | Simple API — one line gives you the result immediately, no await needed | Blocks the main thread on every call, causing jank when data is large or calls are frequent |
| Asynchronous (IndexedDB / Cache API) | Doesn’t block the main thread; handles large data and many operations smoothly | More complex code — you must handle Promises/callbacks and extra error handling |
OPFS createSyncAccessHandle() in a worker | Very fast synchronous I/O without touching the main thread | Only available inside a Web Worker — you must design a separate-thread architecture from the start |
Common mistakes
Section titled “Common mistakes”- Writing to
localStoragerepeatedly in a hot path — e.g. on every keystroke or scroll event — blocks the main thread over and over, causing jank the user can actually feel. - Assuming async always means faster — asynchronous storage doesn’t block the thread, but a single operation can take slightly longer than a synchronous call because of event-loop and IPC overhead.
- Forgetting that
document.cookieis synchronous too — many people focus only onlocalStorage, but parsing a long cookie string on a hot code path blocks the main thread just the same.