IndexedDB: Tutorial for browser storage
Speed is hugely important when surfing the World Wide Web. After all, no-one likes to wait ages for a website to load. To speed up page loading, it’s helpful if some of the information is already stored with the user and doesn’t need to be transmitted. One way of doing this is IndexedDB: storage directly in the user’s browser and accessible to any website. How does it work?
What is IndexedDB used for?
It makes sense that not only servers should store client data, but clients can hold certain website information themselves too. That’s because this speeds up browsing, as everything doesn’t need to be reloaded with each visit. Plus, this allows web applications to also be available offline. User entries can likewise be stored conveniently by the client. Cookies are actually intended for the latter. But they only have a very limited amount of useful storage space – far too limited for modern web applications. What’s more, cookies have to be sent over the web for each HTTP query.
One solution is offered by web storage – also known as DOM storage. This method is still strongly based on the concept of a cookie but is expanded from just a few kilobytes to 10 MB. But even that’s not really much. Moreover, these files, often referred to as super cookies, have a very basic structure. You won't find the properties of a modern database here. However, cookies and super cookies are not only a suboptimal solution due to their small size; both formats don’t permit any structured data or indexes, making searches impossible.
The development of Web SQL initially promised a fundamental change: client-side storage based on SQL. But the World Wide Web Consortium (W3C) – an organisation for developing web standards – stopped the work in favour of IndexedDB. A standard emerged due to the initiative of Mozilla, which is now supported by most modern browsers.
IndexedDB Browser Support
Chrome | Firefox | Opera | Opera mini | Safari | IE | Edge |
---|---|---|---|---|---|---|
What does IndexedDB enable?
In the first instance, the standard is an interface set up in the browser. Websites can use it to store information directly in the browser. This works through JavaScript. Each website is thus able to create its own database. And only the corresponding website is able to access the IndexedDB (short for Indexed Database API). This means data is kept private. Multiple types of object storage are available in the databases. These allow various formats to be stored: strings, numbers, objects, arrays, and data entries.
IndexedDB is an indexed table system rather than a relational database. It’s actually a NoSQL database, much like MongoDB. Entries are always stored in pairs: keys and values. Here, the value concerns an object and the key is the associated property. There are also indexes. They make it possible to perform a quick search.
Actions are always carried out in the form of transactions in IndexedDB. Each write, read or change operation is integrated into a transaction. This ensures that database changes are only performed in full or not at all. An advantage of IndexedDB is that the data transfer doesn’t have to occur synchronously (in most cases). Operations are performed asynchronously. This guarantees that the web browser isn’t disabled during the operation and can still be used by the user.
Security plays an important role when it comes to IndexedDB. It’s necessary to ensure that websites are unable to access the databases of other websites. To this end, IndexedDB established a same-origin policy in which the domain, application layer protocol, and port must be the same, otherwise the data will not be provided. Nonetheless, it’s possible for a subfolder of a domain to access the IndexedDB of another subfolder, as both share the same origin. However, access is not possible if another port is used or the protocol is switched from HTTP to HTTPS, or vice versa.
IndexedDB tutorial: Applying the technology
We explain IndexedDB using an example. Before generating a database and object stores, however, it’s important to integrate a check. Even if IndexedDB is meanwhile supported by almost all modern browsers, this is not the case for outdated web browsers. For this reason, you should ask whether IndexedDB is supported. To do so, you check the window object.
You can trace the code examples via the developer tool console in the browser. The tools also allow you to see the IndexedDBs of other sites.
if (!window.indexedDB) {
alert("IndexedDB is not supported!");
}
If the user’s browser is unable to work with IndexedDB, a dialog window will appear with this information. Alternatively, you can also generate an error report in your logfile using console.error.
You can now open a database. In principle, a website can open multiple databases. But in practice, creating one IndexedDB per domain works best. Here you have the option to use multiple object stores. A database is opened by means of a query – an asynchronous request.
var request = window.indexedDB.open("MyDatabase", 1);
Two arguments can be entered when opening a dataset: first a self-chosen name (as a string) and then the version number (as an integer, i.e. whole number). Logically, you begin with version one. The resulting object provides one of three events:
- error: An error occurred during the generation process.
- upgradeneeded: The version of the database has changed. This likewise appears when creating the database, as this also changes the version number: from non-existent to one.
- success: The database could be successfully opened.
Now the actual database and an object store can be created.
request.onupgradeneeded = function(event) {
var db = event.target.result;
var objectStore = db.createObjectStore("User", { keyPath: "id", autoIncrement: true });
}
Our object store contains the name User. The key is id, a simple numbering system which we can set to increase continuously using autoIncrement. You can now insert data into the database or object store. To do so, first create one or more indexes. In our example, we’d like to create an index for the user name and one for the used email addresses.
objectStore.createIndex("Nickname", "Nickname", { unique: false });
objectStore.createIndex("eMail", "eMail", { unique: true });
This allows you to easily find the datasets based on the pseudonym of a user or their email address. The two indexes differ in that the nickname does not have to be assigned once, but each email address may only be associated with a single entry.
You can now finally add entries. All operations with the database have to be integrated into a transaction. Three different types exist:
- readonly: This reads data from an object store. Multiple transactions of this type can run simultaneously, even if they relate to the same area.
- readwrite: This reads and creates entries. These transactions can only run simultaneously if they relate to different areas.
- versionchange: This carries out changes to the object store or index, but can also create and change entries. This mode can’t be created manually and is instead triggered automatically by the event upgradeneeded.
To create a new entry, use readwrite.
const dbconnect = window.indexedDB.open('MyDatabase', 1);
dbconnect.onupgradeneeded = ev => {
console.log('Upgrade DB');
const db = ev.target.result;
const store = db.createObjectStore('User', { keyPath: 'id', autoIncrement: true });
store.createIndex('Nickname', 'Nickname', { unique: false });
store.createIndex('eMail', 'eMail', { unique: true });
}
dbconnect.onsuccess = ev => {
console.log('DB-Upgrade needed');
const db = ev.target.result;
const transaction = db.transaction('User', 'readwrite');
const store = transaction.objectStore('User');
const data = [
{Nickname: 'Raptor123', eMail: 'raptor@example.com'},
{Nickname: 'Dino2', eMail: 'dino@example.com'}
];
data.forEach(el => store.add(el));
transaction.onerror = ev => {
console.error('An error has occured!', ev.target.error.message);
};
transaction.oncomplete = ev => {
console.log('Data has been added successfully!');
const store = db.transaction('User', 'readonly').objectStore('User');
//const query = store.get(1); // single query
const query = store.openCursor()
query.onerror = ev => {
console.error('Request failed!', ev.target.error.message);
};
/*
// Processing of single query
query.onsuccess = ev => {
if (query.result) {
console.log('Dataset 1', query.result.Nickname, query.result.eMail);
} else {
console.warn('No entry available!');
}
};
*/
query.onsuccess = ev => {
const cursor = ev.target.result;
if (cursor) {
console.log(cursor.key, cursor.value.Nickname, cursor.value.eMail);
cursor.continue();
} else {
console.log('No more entries!');
}
};
};
}
This enables you to insert information into your object store. Moreover, you can use this to display reports via the console, depending on whether the transaction is successful. Any data you have added to an IndexedDB you also generally want to be able to read out. To this end, you can use get.
var transaction = db.transaction(["User"]);
var objectStore = transaction.objectStore("User");
var request = objectStore.get(1);
request.onerror = function(event) {
console.log("Request failed!");
}
request.onsuccess = function(event) {
if (request.result) {
console.log(request.result.Nickname);
console.log(request.result.eMail);
} else {
console.log("No more entries!");
}
};
This code enables you to search for the entry using the key 1 – i.e. the id value 1. If the transaction is unsuccessful, an error report appears. But if the transaction goes through, you find out the content of both entries nickname and email. You’ll also be informed if no entry can be found for the number.
If you’re not just searching for a single entry, but would like multiple results, a cursor can be useful. This function searches for one entry after the other. This way, you can either consider all database entries or select just a certain key area.
var objectStore = db.transaction("User").objectStore("User");
objectStore.openCursor().onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
console.log(cursor.key);
console.log(cursor.value.Nickname);
console.log(cursor.value.eMail);
cursor.continue();
} else {
console.log("No more entries!");
}
};
We created two indexes beforehand to also allow us to call up information using them. This likewise occurs via get.
var index = objectStore.index("Nickname");
index.get("Raptor123").onsuccess = function(event) {
console.log(event.target.result.eMail);
};
Lastly, if you’d like to delete an entry from the database, this works like adding a dataset – using a readwrite transaction.
var request = db.transaction(["User"], "readwrite")
.objectStore("User")
.delete(1);
request.onsuccess = function(event) {
console.log("Entry successfully deleted!");
};