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.
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
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
