IndexedDB, WebSQL, LocalStorage – what blocks the DOM?

When it comes to databases, a lot of people just want to know: which one is the fastest?

Never mind things like memory usage, the CAP theorem, consistency, read vs write speed, test coverage, documentation – just tell me which one is the fastest, dammit!

This mindset is understandable. A single number is easier to grasp than a big table of features, and it’s fun to make grand statements like “Redis is 20x faster than Mongo.” (N.B.: I just made that up.)

As someone who spends a lot of time on browser databases, though, I think it’s important to look past the raw speed numbers. On the client side especially, the way you use a database, and how it interacts with the JavaScript environment, has a big impact on something more important than performance: how your users perceive performance.

In this post, I’m going to take a look at various browser databases with regard not only to their speed, but to how much they block the DOM.

TLDR: IndexedDB isn’t nearly the performance home-run that many in the web community think it is. In my tests, I found that it blocked the DOM significantly in Firefox and Chrome, and was slower than both LocalStorage and WebSQL for basic key-value insertions.

Browser database landscape

For the uninitiated, the world of browser databases can be a confusing one. Lawnchair, PouchDB, LocalForage, Dexie, Lovefield, LokiJS, AlaSQL, MakeDrive, ForerunnerDB, YDN-DB – that’s a lot of databases!

As it turns out, though, the situation is much simpler than it appears on the surface. In fact, there are only three ways of storing data in the browser:

Every “database” listed above uses one of those three under the hood (or they operate in-memory). So to understand browser storage, you only really need to understand LocalStorage, WebSQL, and IndexedDB 1.

LocalStorage is a lightweight way to store key-value pairs. The API is very simple, but usage is capped at 5MB in many browsers. Plus the API is synchronous, so as we’ll see later, it can block the DOM. Browser support is very good.

WebSQL is an API that is only supported in Chrome and Safari (and Android and iOS by extension). It provides an asynchronous, transactional interface to SQLite. Since 2010, it has been deprecated in favor of IndexedDB.

IndexedDB is the successor to both LocalStorage and WebSQL, designed to replace them as the “one true” browser database. It exposes an asynchronous API that supposedly avoids blocking the DOM, but as we’ll see below, it doesn’t necessarily live up to the hype. Browser support is extremely spotty, with only Chrome and Firefox having fully usable implementations.

Now, let’s run a simple test to see when and how these APIs block the DOM.

Thou shalt not block the DOM

JavaScript is a single-threaded programming environment, meaning that synchronous operations are blocking. And since the DOM is synchronous, this means that when JavaScript blocks, the DOM is also blocked. So if any operation takes longer than 16ms, it can lead to dropped frames, which users experience as slowness, “stuttering,” or “jank.”

This is the reason that JavaScript has so many asynchronous APIs. Just imagine if your entire page was frozen during every AJAX request – wouldn’t the web be an awful user experience if it worked that way! Hence the profusion of programming constructs like callbacks, promises, event listeners, and the like.

To demonstrate DOM blocking, I’ve put together a simple demo page with an animated GIF. Whenever the DOM is blocked, Kirby will stop his happy dance and freeze in place.

Try this experiment: go to that page, open up the developer tools, and enter the following code:

for (var i = 0; i < 10000; i++) {console.log('blocked!')}

You’ll see that Kirby freezes for the duration of the for-loop:


This affects more than just animated GIFs; any JavaScript animation or DOM operation, such as adding or modifying elements, will also be blocked. You can’t even select a radio button; the page is totally unresponsive. The only animations that are unaffected are hardware-accelerated CSS animations.

Using this demo page, I tested four ways of of storing data: in-memory, LocalStorage, WebSQL, and IndexedDB. The test inserts a given number of “documents,” which are just unstructured JSON keyed by a string ID. I made a YouTube video showing my results, but the rest of the article will summarize my findings.


Not surprisingly, since any synchronous code is blocking, in-memory operations are also blocking. You can test this in the demo page by choosing “regular object” or “LokiJS” (which is an in-memory database). The DOM blocks during long-running inserts, but unless you’re dealing with a lot of data, you’re unlikely to notice, because in-memory operations are really fast.

To understand why in-memory is so fast, a good resource is this chart of latency numbers every programmer should know. Or I can give you the TLDR, which I’m happy to be quoted on:

“Disk is about a bazillion times slower than memory, and the network is about a bazillion times slower than that.”

— Nolan Lawson

Of course, the tradeoff with in-memory is that your data isn’t saved. So let’s look at some ways of writing data that will actually survive a browser refresh.


In all three of Chrome, Firefox, and Edge, LocalStorage fully blocks the DOM while you’re writing data 2. The blocking is a lot more noticeable than with in-memory, since the browser has to actually flush to disk.

This is pretty much the banner reason not to use LocalStorage. Even if the API only takes a few hundred milliseconds to return after inserting 10000 records, you’ll notice that the DOM might block for a long time after that. I assume this is because these browsers cache LocalStorage to memory and then batch their write operations (here’s how Firefox does it), but in any case the UI still ends up looking janky.

In Safari, the situation is even worse. Somehow the DOM isn’t blocked at all during LocalStorage operations, but on the other hand, if you insert too much data, you’ll get a spinning beach ball of doom, and the page will be permanently frozen. I’ve filed this as a bug on WebKit.


We can only test this one in Chrome and Safari, but it’s still pretty instructive. In Chrome, WebSQL actually blocks the DOM quite a bit, at least for heavy operations. Whereas in Safari, the animations all remain buttery-smooth, no matter what WebSQL is doing.

This should fill you with a sense of foreboding, as we start to move on to the supposed savior of client-side databases, IndexedDB. Aren’t both WebSQL and IndexedDB asynchronous? Don’t they have nothing to do with the DOM? Why should they block DOM rendering at all?

I myself was pretty shocked by these results, even though I’ve worked extensively with these APIs over the past two years. But let’s keep going further and see how deep this rabbit hole goes…


If you try that demo page in Chrome or Firefox, you may be surprised to see that IndexedDB actually blocks the DOM for nearly the entire duration of the operation 3. In Safari, I don’t see this behavior at all (although IndexedDB is painfully slow), whereas in Edge I see the occasional dropped frame.

In both Firefox and Chrome, IndexedDB is slower than LocalStorage for basic key-value insertions, and it still blocks the DOM. In Chrome, it’s also slower than WebSQL, which does blocks the DOM, but not nearly as much. Only in Edge and Safari does IndexedDB manage to run in the background without interrupting the UI, and aggravatingly, those are the two browsers that only partially implement the IndexedDB spec.

This was a pretty shocking find, so I promptly filed a bug both on Chrome and on Firefox. It saddens me to think that this is just one more reason web developers will have to ignore IndexedDB – what with the shoddy browser support and the ugly API, we can now add the fact that it doesn’t even deliver on its promise of beating LocalStorage at DOM performance.

Web workers FTW

I do have some good news: IndexedDB works swimmingly well in a web worker, where it runs at roughly the same speed but without blocking the DOM. The only exception is Safari, which doesn’t support IndexedDB inside a worker.

So that means that for Chrome and Firefox, you can always offload your expensive IndexedDB operations to a worker thread, where there’s no chance of blocking the UI thread. In my own tests, I didn’t see a single dropped frame when using this method.

It’s also worth acknowledging that IndexedDB is the only storage option inside of a web worker (or a service worker, for that matter). Neither WebSQL nor LocalStorage are available inside of a worker for any of the browsers I tested; the localStorage and openDatabase globals just aren’t there. (Support for WebSQL used to exist in Chrome and Safari, but has since been removed.)

Test results

I’ve gathered these results into a consolidated table, along with the time taken in milliseconds as measured by a simple comparison. All tests were on a 2013 MacBook Air; Edge was run in a Windows 10 VirtualBox. “In-memory” refers to a regular JavaScript object (“regular object” in the demo page). Between each test, all browser data was cleared and the page refreshed.

Take these raw numbers with the grain of salt. They only account for the time taken for the API in question to return successfully (or finish the transaction, in the case of IndexedDB and WebSQL), and they don’t guarantee that the data was durably written or that the DOM wasn’t blocked after the operation completed. However, it is interesting to compare the speed across browsers, and it’s pretty consistent with what I’ve seen from working on PouchDB over the past couple of years.

Number of insertions   1000     10000     100000     Blocks?     Notes  
Chrome 47
   In-memory 4 10 217 Yes
   LocalStorage 18 527 4725 Yes
   WebSQL 45 213 1927 Partially Blocks a bit at the beginning
   IndexedDB 64 572 5372 Yes
     in a worker
66 604 6108 No
Firefox 43
   In-memory 1 12 152 Yes
   LocalStorage 19 177 1950 Yes Froze significantly after loop finished
   IndexedDB 114 823 8849 Yes
     in a worker
132 1006 9264 No
Safari 9
   In-memory 2 8 100 Yes
   LocalStorage 6 41 418 No 10000 and 100000 crashed the page
   WebSQL 26 173 1557 No
   IndexedDB 1093 10658 117790 No
Edge 20
   In-memory 7 19 331 Yes
   LocalStorage 198 4624 N/A Yes 100000 crashed the page
   IndexedDB 315 5657 28662 Slightly A few frames lost at the beginning
     in a worker
985 2881 24236 No


Edit: The LocalStorage results are inaccurate, because there was a bug in the test suite causing it to improperly store the JavaScript objects as '[object Object]' rather than using JSON.stringify(). After the fix, LocalStorage performs more poorly.

Key takeaways from the data:

  1. WebSQL is faster than IndexedDB in both Chrome (~2x) and Safari (~100x!) even though I’m inserting unstructured JSON with a string key, which should be IndexedDB’s bread and butter.
  2. LocalStorage is slightly faster than IndexedDB in all browsers (disregarding the crashes).
  3. IndexedDB is not significantly slower when run in a web worker, and never blocks the DOM that way.

Again, these numbers wasn’t gathered in a super rigorous way (I only ran the tests once; didn’t average them or anything), but it should give you an idea of what kind of behavior you can expect from these APIs in different browsers. You can run the demo page yourself to try to reproduce my results.


Running IndexedDB in a web worker is a nice workaround for DOM slowness, but in principle it ought to run smoothly in either environment. Originally, the whole selling point of IndexedDB was that it would improve upon both LocalStorage and WebSQL, finally giving web developers the same kind of storage power that native developers have enjoyed for the past several years.

IndexedDB’s awkward asynchronous API was supposed to be a bitter medicine that, if you swallowed it, would pay off in terms of performance. But according to my tests, that just isn’t the case, at least with IndexedDB’s two flagship browsers, Chrome and Firefox.

I’m still hopeful that browser vendors will resolve all these issues with IndexedDB, although with the spec being over five years old, it sure feels like we’ve been waiting a long time. As someone who does both native and web development for a living, I’m tired of reciting a list of reasons why the web “isn’t quite there yet.” And IndexedDB has been too high on that list for too long.

IndexedDB was the web’s chance to finally get local storage right. It was the chosen one. It was supposed to lead us out of the morass of half-baked solutions and provide the best and fastest way to work with data on the client side. It’s come a long way, but I’m still waiting for it to make good on that original promise.


1: Yes, I’m ignoring cookies, the File API,, SessionStorage, the Service Worker cache, and probably a few other oddballs. There are actually lots of ways of storing data (too many in my opinion), but all of them have niche use cases except for LocalStorage, WebSQL, and IndexedDB.

2: In this article, when I say “Chrome,” “Firefox,” “Edge,” and “Safari”, I mean Chrome Canary 47, Firefox Developer Edition 43, Edge 20, and WebKit Nightly 10600.8.9. All tests were run on a 2013 MacBook Air; Edge was run in Windows 10 using Virtual Box.

3: This blog post used to suggest that the DOM was blocked for the entire duration of the transaction, but after an exchange with Ben Kelly I changed the wording to “nearly the entire duration.”

Thanks to Dale Harvey for providing feedback on a draft of this blog post.

30 responses to this post.

  1. Great article, sad content ;(


  2. I want to comment on two statements you made that – technically – aren’t really the primary point of this article – I hope you don’t mind. If you do, just tell me to stfu and I’ll move on. ;)

    “Originally, the whole selling point of IndexedDB was that it would improve upon both LocalStorage and WebSQL,”

    “IndexedDB is the successor to both LocalStorage and WebSQL”

    Ignoring WebSQL for a moment (that awesome spec that was too easy to live, but whatever ;), to me, I’ve never thought of IDB as the “successor” to LocalStorage. To me, these 2 specs seem like very different features for different purposes. Yes, they both store data on the browser, but it seems like the use cases for both are so different that it isn’t that IDB is better or the successor, it is just different. I see LocalStorage as being excellent for simple settings and caching atomic pieces of data, while IDB has always felt like the storage system for dynamic data (i.e., I don’t know how big it is going to be).

    Obviously I can’t speak for what the folks behind the spec thought, but I see use in both types of storage and I don’t think it needs to be a “you must only use one” type thing.


    • Hm, from what I’ve heard from most browser vendors, IndexedDB is designed to replace them both (citation needed, though). The fact that IDB is the only storage option in a web worker/service worker seemed to make a strong statement about what they think is the right path moving forward. I doubt LocalStorage will ever be removed from browsers (or WebSQL for that matter) due to so many sites using it, but I tend to hear it talked about in much the same way as AppCache (not going away, but try to avoid it).

      But yes, in practical terms, IDB is just different from LocalStorage, whereas it’s got more-or-less feature-parity with WebSQL. I still like LocalStorage a lot; the API is way easier to grok than either IDB or WebSQL.


  3. Posted by asutherlandorg on September 30, 2015 at 11:40 AM

    It’s worth calling out that LocalStorage, at least on Firefox, isn’t so much a database as an aggressively pre-loaded in-memory dictionary that gets persisted to disk.

    When Firefox/Gecko sees it’s going to load a URL in a domain/origin, it pokes the local storage code to load the data in the background. If your code loads before all of the keys/values have loaded and tries to perform a localstorage manipulation, then your JS will synchronously block until they are loaded, even if the specific key you want has been loaded. This is notable because one potential takeaway from this blog post is that “localstorage is fast, use that if your data isn’t too huge”, but the more you put in there, the slower your startup is going to be the first time a page is loaded under your origin. (That is, even if you don’t touch localstorage until it’s fully loaded, you’re still potentially forcing the browser to load N megs of data from disk at the same time it might otherwise be trying to read things out of disk cache, etc. etc.)

    The contents of localstorage then remain in memory in their entirety until the page has been closed plus some additional “what if the page comes back” factor on the order of 20 seconds. So no favors are being done for the user memory-usage-wise.

    Write-wise, in Firefox, localstorage writes do happen asynchronously in the background. The primary issue we’d expect under non-e10s (multiprocess) runtime scenarios is extreme lock contention for scheduling the writes as they’re issued. While locks are not held while doing the I/O, for localstorage 100,000 times an async operation needs to be scheduled and put on a queue.

    Unfortunately, it’s also very conceivable for there to be secondary effects as other parts of the browser may be impacted by the effective I/O storm occurring, especially as fsync()s are generated. The worst case would be if something like Places (it powers the awesomebar/link colorizing/etc.) that used to occasionally force the main thread to (synchronously) join on an asynchronous database thread that is getting backed up because of I/O contention. I feel like places may be all fixed now as the synchronous API consumers were replaced, but I could be wrong…


    • Thanks for the insights! The preloading is mentioned in the link I posted above, but I wasn’t aware of how writes are scheduled.

      For the record, I’m definitely not advocating “use LocalStorage instead of IndexedDB.” If a jankless UI is your goal, it’s starting to look like the ideal combo is 1) IDB+WebWorker for Chrome/FF, 2) Regular IDB for IE/Edge, and 3) WebSQL for Safari. (For why I’m not sold on IDB+WebWorker for IE, see this PouchDB bug).


  4. Agree the content is sad, but thanks for the tests!


  5. The reason the DOM appears blocked in these tests is that each of the individual requests (puts, gets, etc) generates an event that fires back at the main thread. When loading up a database with 100k records, that’s 100k events. In Chrome, at least, I measured some event delivery at <35us per event on my box, but that still chews up multiple seconds on the main loop to acknowledge and attempt to deliver all of the events – 35us/event * 100k events = 3.5s, which is pretty close to the 5s you’re seeing.

    One interesting observation is that the browsers that don’t “block” – Safari and Edge – also take an order of magnitude longer to complete the transaction. I suspect that Safari and Edge throttle the database event delivery to avoid starving the DOM, at the expense of database performance. Chrome doesn’t throttle – all the events just get dumped on the thread and the DOM waits. I suspect FF behaves similarly.

    Chrome is improving its internal scheduler; we could do a better job of de-prioritizing the database event delivery if the DOM is also wanting to do some work. We can also try and reduce the per-event overhead. (This would possibly have some overall throughput reduction, but we’d need to measure.)

    We’re also brainstorming ways to improve the API to avoid this problem. When bulk-loading a database, you don’t actually care about those 100k success events, just that the whole transaction succeeded. explores one approach. A dedicated bulk-load function like putAll() is another tact, analogous to the proposed getAll() from “IDB 2nd Ed”. A third tact would be better primitives for splitting up the transaction’s work to avoid janking the main thread, e.g. via transaction.waitUntil()


    • Thanks for the feedback! I definitely don’t mean to knock FF/Chrome’s implementations; they’re way more solid than IE’s and especially Safari’s.

      Also as Ben Kelly mentioned in the public-webapps thread, the put() locking is alleviated if you break it up into separate transactions, although it does takes longer. (See these performance tests for some comparisons of multi-transaction vs single-transaction, corresponding to PouchDB’s put() vs bulkDocs().)

      I think your waitUntil() Promise proposal is awesome, and would be a big help to let the event loop breathe a bit while still doing a single transaction. txn.commit() also makes sense, although PouchDB does occasionally make use of individual put() error events (most of the action is in this code). putAll() would also be nice, if for no other reason than that’s what I find myself essentially doing in a lot of PouchDB code – calling put() a whole bunch inside the same transaction.

      I’m also starting to think it’s just wise in general for IDB wrappers to defer to worker threads where possible. I implemented exactly that for PouchDB and the gains are pretty tremendous, especially when you factor in PouchDB’s non-IDB stuff that may block (e.g. md5 checksumming for binary attachments). I think most IDB wrappers like Dexie or LocalForage could do the same thing without changing their APIs.


      • I should follow up and note that I wasn’t correct in my analysis. I replied on but to loop back here…

        I did more timing analysis on Nolan’s case, hoping to find ways to optimize or throttle the event dispatch. I learned that it wasn’t the event flood causing the jank here! I’d seen that in other performance reports, and just assumed it applied here as well. Bad me. Chrome was able to dispatch 100k events in just a handful of ms.

        The 3.5ms breaks down into roughly:

        1s serializing – converting the live JS object into a byte stream that can be written to disk
        1s extracting the inline key – which involves deserializing the byte stream back into a JS object (to avoid getter/setter hi-jinks)
        1s setting up the async call (converting data types, copying buffers, etc)
        (the rest is spread across various error checks and type conversions)

        (Remember, that’s with 100k calls, so each of those steps is taking only 10 microseconds per call.)

        All of this must be done synchronously, as the spec requires synchronous errors on failure for the first two steps (and we can’t operate truly async on JS data anyway – it all must happen on the main thread). We do know serializing/deserializing performance could be improved in Chrome, and there is likely some time we can squeeze out of the async call setup.

        But even if we managed to reduce the time by 50% (and we should try!), that’s still a ~2s jank, which is not acceptable for a responsive page. So pages should really avoid trying to do so much work in a single frame. One approach in Indexed DB is to batch the work:

        open a transaction
        if there are no more records to write, stop!
        record the current time
        issue put() calls until N ms have passed (e.g. 3ms budget)
        attach a callback to the onsuccess of the last put()
        when the callback fires, go to step 2

        This will all take place within a single transaction, but should give the DOM a chance to “breathe”. It will necessarily be slower than a single huge bulk write, but the DOM and your script are competing for the main thread so it’s a trade-off when making responsive pages.

  6. […] Lawson wanted to know more about the performance of in-browser storage18 and how it affects the DOM. Turns out it’s complicated and varies from browser to browser and […]


  7. […] IndexedDB, WebSQL, LocalStorage – What Blocks The DOM? […]


  8. […] IndexedDB, WebSQL, LocalStorage – what blocks the DOM? – In my tests, I found that IndexedDB blocked the DOM significantly in Firefox and Chrome, and was slower than both LocalStorage and WebSQL for basic key-value insertions. […]


  9. […] IndexedDB, WebSQL, LocalStorage – what blocks the DOM? […]


  10. Posted by Eliel on December 20, 2015 at 1:27 AM

    Hi Nolan very nice and useful article, thank you for sharing it. One question: I’m starting to develop a kind of Exam Prep app. I want to make it suitable to run offline and with no connection with internet at all (question database shall be updated eventually though) in order to facilitate things to students and other stuff. I’m using hybrid technologies in order to cut down the development time, however I’m sitting and stuck on the decision of what approach to use for storing the data. Can’t use WebSql due the support and not sure if IndexedDB is the best to do….I don’t have other option available I guess. Please a kind suggestion would be appreciated from you all guys.


  11. […] 这个程序,我决定使用[PouchDB](保存宠物小精灵数据(因为它的同步良好),同时使用LocalForage作为应用的状态数据存储(因为它有一个很好的键值API)。无论 PouchDB 和 LocalForage 使用 IndexedDB 的 Web Worder,这意味着任何数据库操作者将是完全无阻塞。 […]


  12. […] 这个程序,我决定使用[PouchDB]( /)保存口袋妖怪数据(因为它擅长同步),同时使用[LocalForage]( /localForage)作为应用的状态数据存储(因为它有一个很好的键值API(key-value API))。 PouchDB 和 LocalForage 都在 web worker 中使用 IndexedDB,这意味着任何数据库操作者将是完全无阻塞。 […]


  13. […] 这个程序,我决定使用[PouchDB]( /)保存口袋妖怪数据(因为它擅长同步),同时使用[LocalForage]( /localForage)作为应用的状态数据存储(因为它有一个很好的键值API(key-value API))。 PouchDB 和 LocalForage 都在 web worker 中使用 IndexedDB,这意味着任何数据库操作者将是完全无阻塞。 […]


  14. […] do aplicativo foi efetuar o mínimo de operações possíveis no banco de dados pois essas bloqueiam o DOM e iriam travar minha interface. De início segui as dicas do artigo How To Use PouchDB + SQLite For […]


  15. […] mask slow operations!) The query is also run in a web worker, which is why you won’t see any UI blocking from IndexedDB during database […]


  16. […] 这个程序,我决定使用[PouchDB]( /)保存口袋妖怪数据(因为它擅长同步),同时使用[LocalForage]( /localForage)作为应用的状态数据存储(因为它有一个很好的键值API(key-value API))。 PouchDB 和 LocalForage 都在 web worker 中使用 IndexedDB,这意味着任何数据库操作者将是完全无阻塞。 […]


  17. […] recent posts and talks, I’ve explored how Web Workers can vastly improve the responsiveness of a web […]


  18. Posted by Danish Shrestha on June 20, 2016 at 2:30 PM

    Great article. Thanks for putting it together. I have a quick question. What is the unit for the table? is it in seconds or milliseconds?



  19. Great article !! thanks for explaining things really well !! :)


  20. […] IndexedDB, WebSQL, LocalStorage – what blocks the DOM?: […]


  21. […] Lawson wanted to know more about the performance of in-browser storage18 and how it affects the DOM. Turns out it’s complicated and varies from browser to browser and […]


  22. Posted by Aquib Vadsaria on August 30, 2016 at 1:28 AM

    absolutely amazing article to get a deep insights of brower client side storage, though will do a much thorough test of all the mentioned facts.


  23. Greate post. Keep writing such kind of info
    on your page. Im really impressed by your blog.

    Hi there, You have performed a fantastic job. I
    will definitely digg it and personally suggest to my friends.
    I’m sure they’ll be benefited from this web site.


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: