Mastering IndexedDB: Efficient Storage, Bulk Operations, and Multi‑Tab Strategies

This article explores using IndexedDB for large‑scale client‑side data storage, covering database creation, CRUD wrappers, bulk inserts, performance testing, and multi‑tab coordination via BroadcastChannel and leader election, while providing practical code samples and optimization tips for robust offline logging.

NetEase Smart Enterprise Tech+
NetEase Smart Enterprise Tech+
NetEase Smart Enterprise Tech+
Mastering IndexedDB: Efficient Storage, Bulk Operations, and Multi‑Tab Strategies

Introduction

When a JavaScript program needs to store data in the browser, options include Cookie, LocalStorage, WebSQL, FileSystem API, and IndexedDB. IndexedDB is the only option suitable for large‑scale storage because it is a NoSQL database that supports asynchronous operations, transactions, JSON data, and indexing.

Problems with Direct IndexedDB Use

Low‑level transactional API is verbose and needs simplification.

Performance bottleneck is mainly the number of transactions.

Multiple tabs can issue concurrent operations on the same records.

Database Creation Example

class Database {
  constructor(options = {}) {
    if (typeof indexedDB === 'undefined') {
      throw new Error('indexedDB is unsupported!');
    }
    this.name = options.name;
    this.db = null;
    this.version = options.version || 1;
  }
  createDB() {
    return new Promise((resolve, reject) => {
      indexedDB.deleteDatabase(this.name);
      const request = indexedDB.open(this.name);
      request.onupgradeneeded = () => {
        const db = request.result;
        this.createStore(db);
      };
      request.onsuccess = () => {
        this.db = request.result;
        resolve(request.result);
      };
      request.onerror = event => reject(event);
    });
  }
  createStore(db) {
    if (!db.objectStoreNames.contains('log')) {
      const objectStore = db.createObjectStore('log', {
        keyPath: 'id',
        autoIncrement: true
      });
      objectStore.createIndex('time', 'time');
    }
  }
}

Usage:

(async function () {
  const database = new Database({ name: 'db_test' });
  await database.createDB();
  console.log(database);
})();

CRUD Wrapper

class Database {
  // ... constructor and createDB omitted
  add(data) {
    return new Promise((resolve, reject) => {
      const db = this.db;
      const transaction = db.transaction('log', 'readwrite');
      const store = transaction.objectStore('log');
      const request = store.add(data);
      request.onsuccess = evt => resolve(evt.target.result);
      request.onerror = evt => reject(evt);
    });
  }
  put(data) {
    return new Promise((resolve, reject) => {
      const db = this.db;
      const transaction = db.transaction('log', 'readwrite');
      const store = transaction.objectStore('log');
      const request = store.put(data);
      request.onsuccess = evt => resolve(evt.target.result);
      request.onerror = evt => reject(evt);
    });
  }
  delete(id) {
    return new Promise((resolve, reject) => {
      const db = this.db;
      const transaction = db.transaction('log', 'readwrite');
      const store = transaction.objectStore('log');
      const request = store.delete(id);
      request.onsuccess = evt => resolve(evt.target.result);
      request.onerror = evt => reject(evt);
    });
  }
  get(value, indexName) {
    return new Promise((resolve, reject) => {
      const db = this.db;
      const transaction = db.transaction('log', 'readwrite');
      const store = transaction.objectStore('log');
      let request;
      if (indexName) {
        const index = store.index(indexName);
        request = index.get(value);
      } else {
        request = store.get(value);
      }
      request.onsuccess = evt => resolve(evt.target.result || null);
      request.onerror = evt => reject(evt);
    });
  }
  getByIndex(keyPath, keyRange, offset = 0, limit = 100) {
    return new Promise((resolve, reject) => {
      const db = this.db;
      const transaction = db.transaction('log', 'readonly');
      const store = transaction.objectStore('log');
      const index = store.index(keyPath);
      const request = index.openCursor(keyRange);
      const result = [];
      request.onsuccess = function (evt) {
        const cursor = evt.target.result;
        if (offset > 0) {
          cursor.advance(offset);
        }
        if (cursor && limit > 0) {
          result.push(cursor.value);
          limit--;
          cursor.continue();
        } else {
          resolve(result);
        }
      };
      request.onerror = evt => reject(evt.target.error);
      transaction.onerror = evt => reject(evt.target.error);
    });
  }
}

Bulk Insert

class Model {
  bulkPut(datas) {
    if (!datas || datas.length === 0) {
      return Promise.reject(new Error('no data'));
    }
    return new Promise((resolve, reject) => {
      const db = this.db;
      const transaction = db.transaction('log', 'readwrite');
      const store = transaction.objectStore('log');
      datas.forEach(data => store.put(data));
      transaction.oncomplete = () => resolve();
      transaction.onabort = evt => reject(evt.target.error);
    });
  }
}

Performance Experiments

Inserting 1 000 records with 1 000 separate transactions took about 20 s, while a single transaction with 1 000 records completed in roughly 250 ms. Reducing the number of transactions therefore yields a dramatic speed improvement.

Further tests with up to 5 million records showed only modest variations; bulk insertion remained consistently fast.

Multi‑Tab Coordination

Because IndexedDB processes transactions regardless of their originating tab, concurrent writes can cause errors. A leader‑election pattern using BroadcastChannel (or SharedWorker where supported) can ensure that only one tab performs writes.

class LeaderElection {
  constructor(name) {
    this.channel = new BroadcastChannel(name);
    this.hasLeader = false;
    this.isLeader = false;
    this.tokenNumber = Math.random();
    this.maxTokenNumber = 0;
    this.channel.onmessage = evt => {
      const action = evt.data.action;
      switch (action) {
        case 'applyReject':
          this.hasLeader = true;
          break;
        case 'leader':
          this.hasLeader = true;
          break;
        case 'death':
          this.hasLeader = false;
          this.maxTokenNumber = 0;
          break;
        case 'apply':
          if (this.isLeader) {
            this.postMessage('applyReject');
          } else if (!this.hasLeader && evt.data.tokenNumber > this.maxTokenNumber) {
            this.maxTokenNumber = evt.data.tokenNumber;
          }
          break;
        default:
          break;
      }
    };
  }
  // awaitLeadership, applyOnce, beLeader, die, postMessage, sleep omitted for brevity
}

Usage:

const elector = new LeaderElection('test_channel');
window.elector = elector;
elector.awaitLeadership().then(() => {
  document.title = 'leader!';
});

Conclusion

IndexedDB’s low‑level transactional API is cumbersome and benefits from a thin wrapper.

The main performance bottleneck is the number of transactions; batch writes are strongly recommended.

In multi‑tab environments, elect a leader to serialize writes and avoid conflicts.

The source code is available on GitHub: https://github.com/everlose/indexeddb-test

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PerformanceJavaScriptIndexedDBmulti-tabbrowser storagebulk operations
NetEase Smart Enterprise Tech+
Written by

NetEase Smart Enterprise Tech+

Get cutting-edge insights from NetEase's CTO, access the most valuable tech knowledge, and learn NetEase's latest best practices. NetEase Smart Enterprise Tech+ helps you grow from a thinker into a tech expert.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.