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 in­form­a­tion is already stored with the user and doesn’t need to be trans­mit­ted. One way of doing this is IndexedDB: storage directly in the user’s browser and ac­cess­ible 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 in­form­a­tion them­selves too. That’s because this speeds up browsing, as everything doesn’t need to be reloaded with each visit. Plus, this allows web ap­plic­a­tions to also be available offline. User entries can likewise be stored con­veni­ently 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 ap­plic­a­tions. 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 prop­er­ties of a modern database here. However, cookies and super cookies are not only a sub­op­tim­al solution due to their small size; both formats don’t permit any struc­tured data or indexes, making searches im­possible.

The de­vel­op­ment of Web SQL initially promised a fun­da­ment­al change: client-side storage based on SQL. But the World Wide Web Con­sor­ti­um (W3C) – an or­gan­isa­tion for de­vel­op­ing web standards – stopped the work in favour of IndexedDB. A standard emerged due to the ini­ti­at­ive 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 in­form­a­tion directly in the browser. This works through JavaS­cript. Each website is thus able to create its own database. And only the cor­res­pond­ing 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 re­la­tion­al 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 as­so­ci­ated property. There are also indexes. They make it possible to perform a quick search.

Actions are always carried out in the form of trans­ac­tions in IndexedDB. Each write, read or change operation is in­teg­rated into a trans­ac­tion. 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 syn­chron­ously (in most cases). Op­er­a­tions are performed asyn­chron­ously. This guar­an­tees 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 es­tab­lished a same-origin policy in which the domain, ap­plic­a­tion layer protocol, and port must be the same, otherwise the data will not be provided. Non­ethe­less, 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 tech­no­logy

We explain IndexedDB using an example. Before gen­er­at­ing 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.

Note

You can trace the code examples via the developer tool console in the browser. The tools also allow you to see the In­dexed­DBs 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 in­form­a­tion. Al­tern­at­ively, 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 asyn­chron­ous 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 gen­er­a­tion process.
  • up­graden­eeded: 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 suc­cess­fully 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 con­tinu­ously using autoIn­cre­ment. 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 as­so­ci­ated with a single entry.

You can now finally add entries. All op­er­a­tions with the database have to be in­teg­rated into a trans­ac­tion. Three different types exist:

  • readonly: This reads data from an object store. Multiple trans­ac­tions of this type can run sim­ul­tan­eously, even if they relate to the same area.
  • readwrite: This reads and creates entries. These trans­ac­tions can only run sim­ul­tan­eously if they relate to different areas.
  • ver­sion­change: 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 auto­mat­ic­ally by the event up­graden­eeded.

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 in­form­a­tion into your object store. Moreover, you can use this to display reports via the console, depending on whether the trans­ac­tion is suc­cess­ful. 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 trans­ac­tion is un­suc­cess­ful, an error report appears. But if the trans­ac­tion 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 be­fore­hand to also allow us to call up in­form­a­tion 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 trans­ac­tion.

var request = db.transaction(["User"], "readwrite")
    .objectStore("User")
    .delete(1);
request.onsuccess = function(event) {
    console.log("Entry successfully deleted!");
};
Summary

This article has in­tro­duced you to the first steps with IndexedDB. You can find more in­form­a­tion at Mozilla or Google, for example. In the example code, however, Google uses a special library, which is why the code is slightly different from the Mozilla code.

Go to Main Menu