Why Your App State Should Live in IndexedDB

Reconstructing application state on every visit is expensive. A user typically has to be authenticated and multiple API requests have to complete before the first render. Persisting that state in IndexedDB lets returning visitors get a usable UI almost immediately, while background processes revalidate the data against your services using a stale-while-revalidate strategy.

That approach is powerful, but the IndexedDB API has edge cases that aren't obvious at first glance. Below are the practices that matter most when you use IndexedDB to persist client-side application state.

Design for Failure

The biggest source of IndexedDB complexity is that you don't control the environment. Browser extensions, developer tools, and users themselves can all interfere with your stored data. More importantly, writes can fail for reasons that have nothing to do with your code quality.

Writes Can and Will Fail

Some browsers block IndexedDB writes entirely in private browsing mode. A device that is nearly out of disk space may cause the browser to reject any new storage. Your write code must therefore always include robust error handling that assumes failure is a normal outcome, not an exception.

This is also a strong argument for keeping application state in memory as the source of truth for the UI. IndexedDB should be a persistence layer, not a dependency. That way the interface still renders correctly in private browsing or when storage is unavailable, even if the features that require saving data are disabled.

Your Data Is Not Sacred

A user can clear their site data at any moment. They can also open DevTools and mutate records directly. Your app needs to handle missing, modified, or deleted records without crashing. Treat any stored data as a cache that may be invalidated at any time, and make sure the UI has a defined behavior for the "nothing cached" case.

Schema Evolution Is a Real Job

Stored data is always a snapshot from a past version of your code, which may have had bugs. IndexedDB provides schema versioning through IDBOpenDBRequest.onupgradeneeded(), but that only helps if your upgrade logic is written defensively.

Your migration code must correctly handle every previous version a user could be coming from including versions that wrote corrupt data. Browsers will not clean up after you. Since manually testing every migration path is impractical, unit tests that jump from each legacy version to the current one are essential.

Keep Writes Small and Targeted

IndexedDB's API is asynchronous, but that does not mean it is free from main-thread work. Structured cloning, which IndexedDB must perform when storing any object, runs on the main thread. If you store large objects, that clone blocks the main thread, and your page will jank or freeze.

To keep things fast, read and write only what you actually need. From a performance perspective, the size and frequency of your writes matter far more than the asynchronous nature of the calls.

Don't Store the Whole State Tree

The common state management pattern, typified by Redux, is to hold the entire state object in a single JavaScript object. It is tempting to write that whole tree to IndexedDB as one record after each update, even with throttling or debouncing. Don't.

Storing a single massive, nested record after every change increases the chance of write failures, blocks the main thread with large structured clones, and in extreme cases makes the tab unresponsive or crashes it. Break the state into individual records or logical sub-trees and update only the records that actually changed.

When splitting your state into fine-grained records is not feasible, writing separate sub-trees is still far better than serializing the entire state each time. Small improvements are worthwhile.

Finally, verify that your persistence layer is actually improving the experience. Measure your performance and check for long tasks that stall the main thread. If your writes are small enough that they never cause jank, you have already solved the problem. If they are not, optimize the ones that are causing the measurable slowdown first.

The Bottom Line

Using IndexedDB to persist application state can dramatically reduce startup time on repeat visits. But the same storage layer that speeds up your app can break it if you ignore failure modes or write inefficiently.

Client-side storage is an environment you cannot fully control. Build your persistence layer to handle pending writes, corrupted data, and storage denial gracefully, and keep your code covered with tests that simulate outdated schemas. Your users will get a faster experience that never sacrifices reliability.