A (slightly) better approach to using IndexedDB
October 14, 2023 · 5 min read

I've worked with IndexedDB a lot, and along the way, made some mistakes and found better ways to use it. Some of the drawbacks I and the people I worked with have told me are

  • It's slow - I've come to realize it has a lot to do with the code than IDb itself.
  • No events - IDb does not provide a way to let me know that some data has changed.
  • Code complexity - the onsuccess and onerror functions can become hard to work with after a point.

Every data store has it's own advantages and disadvantages. If used correctly, IDb is a very good tool to store and retrieve huge amount of structured data which otherwise is too much for Local/Session storage to handle.

Define the schema

By defining the schema I do not mean hire a DB expert and structure your tables. What I mean is have some sense of how you want to structure it, organize it, shard it, split it into smaller object stores.

An engineer who can organize their desk can organize an IndexedDB database.

IDb does not have in-built support of SQL like "joins", so make sure you do not rely on your data being in multiple object stores, or you have to execute multiple queries as seen with other key-value pair databases (DynamoDB).

In-memory is faster

Unlike SQL, where you can write a WHERE col_1 = %{string}% AND col_2 >= {number} to get what you want, querying an IDb store is painful. So instead of trying to figure out how to write a complex query using cursors, if possible - use in-memory searches. They are much faster and easier to implement.

TL;DR: do a getAll on the object store, put it in a variable and execute your search on that array.

There are a lot of freemium tools like RxDB that work like this. They are in-memory stores that use IDb for persistance and fault tolerance.

Single transactions

If you have worked with any backend service that talks to a database, you would have implemented something called connection pooling. Opening DB connections and initiating a transaction is an expensive operation. This is where a lot of people say that IDb is slow. Every single write operation opens up a new transaction and then writes to the object store - do not do this. Instead, open up a single transaction that has readwrite access to multiple object stores and frequent writes can be batched together.

Reusing transactions can drastically improve write performance in IDb. IDb is fast when it comes to writing. It can write 1,000 documents in double digit milliseconds.

IDb transactions can have multiple object stores passed to them in an array, like below

IDbConnection.result.transaction(['store1', 'store2', ..., 'readwrite'])

You can use RxJS's buffer to batch together queries if they are coming from different sources.

Different transaction for read and write

Title explains it 🤷🏻‍♂️. IDb supports two types of transactions for queries - readonly and readwrite. Pretty self explanatory. But using a readwrite transaction to read and write simultaneously is a performance bottle neck. Read queries will wait in queue for write to be completed and will be slower to respond.

Good old classes for abstraction

IDb API is a bit messy, so a good abstraction is needed to hide away all the complex code and expose simple APIs for use. I rely heavily on classes for this. Every table (or object store) will be an object (JS object) that simply exposes functions like insert, get, update and remove. There's a lot of open source projects that do this for you, but I like to use my own implementation, as it is suited for mine and my teams' needs.

You can checkout DixieJS if you do not want to build one on your own.

Finally, RxJS for reactivity

I've used, and loved RxJS for a while. One of the reasons I've used it is to create a "reactive" IndexedDB. You create a wrapper for IDb (pretty simple if you get the hang of it), and expose a subscribe function from the object to use it anywhere and to listen to IDb query events. I've written down a simple implementation below:

export class IDbWrapper {

  private emitter // JS EventEmitter

  constructor () {
    // create your connections and transactions
  }

  insert () {
    // your abstraction for IDb add/put
    query.onsuccess = () => {
      emitter.emit({ query: 'INSERT', data, ... })
    }
  }

  subscribe () {
    return fromEvent(this.emitter, 'eventName').subscribe
  }
}

All you need to do is call the subscribe method wherever you want.

Make sure the IDb wrapper is a singleton object. We do not want multiple objects creating chaos :)


All of the above stuff has helped me rely on IndexedDB extensively for all my web storage needs.