Measuring localStorage Performance

February 11, 2014 4:59 pm | 3 Comments

Recently someone asked me if it was possible to measure the performance of localStorage. While this was difficult a few years ago, we can do it now thanks to Navigation Timing. This post explains how to measure localStorage performance as well as the results of my tests showing the maximum size of localStorage, when the penalty of reading localStorage happens, and how localStorage behavior varies across browsers.

Past Challenges

In 2011 & 2012, Nicholas Zakas wrote three blog posts about the performance of localStorage. In the last one, The performance of localStorage revisited, he shared this insight from Jonas Sicking (who worked on localStorage in Firefox) explaining why attempts to measure localStorage up to that point were not accurate:

Firefox starts out by reading all of the data from localStorage into memory for the page’s origin. Once the data is in memory, reads and writes should be relatively fast (…), so our measuring of reads and writes doesn’t capture the full picture.

Nicholas (and others) had tried measuring the performance of localStorage by placing timers around calls to localStorage.getItem(). The fact that Firefox precaches localStorage means a different approach is needed, at least for Firefox.

Measuring localStorage with Navigation Timing

Early attempts to measure localStorage performance didn’t capture the true cost when localStorage is precached. As Jonas described, Firefox precaches localStorage, in other words, it starts reading a domain’s localStorage data from disk when the browser first navigates to a page with that domain. When this happens the performance of localStorage isn’t captured because it might already be in memory before the call to getItem().

My hypothesis is that we should be able to use Navigation Timing to measure localStorage performance, even in browsers that precache it. Proving this hypothesis would let us measure localStorage performance in Firefox, and determine if any other browsers have similar precaching behavior.

The Navigation Timing timeline begins with navigationStart – the time at which the browser begins loading a page. Reading localStorage from disk must happen AFTER navigationStart. Even with this knowledge, it’s still tricky to design an experiment that measures localStorage performance. My experiment includes the following considerations:

  • Fill localStorage to its maximum so that any delays are more noticeable.
  • Use two different domains. In my case I use st2468.com and stevesouders.com. The first domain is used for storing and measuring localStorage. The second domain is for landing pages that have links to the first domain. This provides a way to restart the browser, go to a landing page on stevesouders.com, and measure the first visit to a page on st2468.com.
  • Restart the browser and clear the operating system’s disk cache between measurements.
  • In the measurement page, wrap the getItem() calls with timers as well as recording the Navigation Timing metrics in order to see when the precache occurs. We know it’s sometime after navigationStart but we don’t know what marker it’s before.
  • Make the measurement page cacheable. This removes any variability due to network activity.

The step-by-step instructions can be seen on my test page. I use Browserscope to record the results but otherwise this test is very manual, especially since the only reliable way to clear the OS disk cache on Windows, iOS, and Android is to do a power cycle (AFAIK). On the Macbook I used the purge command.

The Results: maximum localStorage

The results of filling localStorage to the maximum are shown in Table 1. Each browser was tested nine times and the median value is shown in Table 1. (You can also see the raw results in Browserscope.) Before determining if we were able to capture Firefox’s precaching behavior, let me describe the table:

  • The User Agent column shows the browser and version. Chrome, Firefox, Opera, and Safari were tested on a Macbook Air running 10.9.1. IE was tested on a Thinkpad running Windows 7. Chrome Mobile was tested on a Samsung Galaxy Nexus running Android 4.3. Mobile Safari was tested on an iPhone 5 running iOS 7.
  • The size column shows how many characters localStorage accepted before throwing a quota exceeded error. The actual amount of space depends on how the browser stores the strings – single byte, double byte, mixed. The number of characters is more relevant to most developers since everything saved to localStorage is converted to a string. (People using FT Labs’s ftdatasquasher might care more about the actual storage mechanism underneath the covers.)
  • The delta getItem column shows how long the call to getItem() took. It’s the median of the difference between “AFTER getItem time” and “BEFORE getItem time”. (In other words, it’s possible that the difference of the medians in the table don’t equal the “delta getItem” median exactly. This is an artifact of how Browserscope displays results. Reviewing the raw results shows that if the math isn’t exact it’s very close.)
  • The remaining columns are markers from Navigation Timing, plus the manual markers before and after the call to getItem(). The value is the number of milliseconds at which that marker took place relative to navigationStart. For example, in the first row responseStart took place 3 ms after navigationStart. Notice how responseEnd takes place just 2 ms later because this page was read from cache (as mentioned above).

One thing to notice is that there are no Navigation Timing metrics for Safari and Mobile Safari. These are the only major browsers that have yet to adopt the W3C Navigation Timing specification. I encourage you to add your name to this petition encouraging Apple to support the Navigation Timing API. For these browsers, the before and after times are relative to a marker in an inline script at the top of the HEAD.

Table 1. maximum localStorage
User Agent size (K-chars) delta getItem (ms) response- Start time response- End time dom- Loading time BEFORE getItem time AFTER getItem time dom- Interactive time
Chrome 33 5,120 1,038 3 5 21 26 1,064 1,065
Chrome Mobile 32 5,120 1,114 63 69 128 163 1,314 1,315
Firefox 27 5,120 143 2 158 4 15 158 160
IE 11 4,883 759 3 3 3 15 774 777
Opera 19 5,120 930 2 4 14 20 950 950
Mobile Safari 7 2,560 453 1 454
Safari 7 2,560 520 0 520

Did we capture it?

The results from Table 1 show that Firefox’s localStorage precaching behavior is captured using Navigation Timing. The delta of responseStart and responseEnd (the time to read the HTML document) is 156 ms for Firefox. This doesn’t make sense since the HTML was read from cache. This should only take a few milliseconds, which is exactly what we see for all the other browsers that support Navigation Timing (Chrome, IE, and Opera).

Something else is happening in Firefox during the loading of the HTML document that is taking 156 ms. The likely suspect is Firefox precaching localStorage. To determine if this is the cause we reduce the amount of localStorage data to 10K. These results are shown in Table 2 (raw results in Browserscope). With only 10K in localStorage we see that Firefox reads the HTML document from cache in 13 ms (responseEnd minus responseStart). The only variable that changed between these two tests was the amount of data in localStorage: 10K vs 5M. Thus, we can conclude that the increase from 13 ms to 156 ms is due to Firefox precaching taking longer when there is more localStorage data.

Table 2. 10K localStorage
User Agent size (K-chars) delta getItem (ms) response- Start time response- End time dom- Loading time BEFORE getItem time AFTER getItem time dom- Interactive time
Chrome 33 10 3 5 7 18 28 29 29
Chrome Mobile 32 10 28 73 76 179 229 248 250
Firefox 27 10 1 3 16 4 15 16 16
IE 11 10 15 6 6 6 48 60 57
Opera 19 10 7 2 4 15 23 33 33
Mobile Safari 7 10 16 1 17
Safari 7 10 11 0 11

Using Navigation Timing we’re able to measure Firefox’s precaching behavior. We can’t guarantee when it starts but presumably it’s after navigationStart. In this experiment it ends with responseEnd but that’s likely due to the page blocking on this synchronous disk read when the call to getItem() is reached. In the next section we’ll see what happens when the call to getItem() is delayed so there is not a race condition.

Does anyone else precache localStorage?

We discovered Firefox’s precaching behavior by comparing timings for localStorage with 10K versus the maximum of 5M. Using the same comparisons it appears that none of the other browsers are precaching localStorage; the delta of responseStart and responseEnd for all other browsers is just a few milliseconds. We can investigate further by delaying the call to getItem() until one second after the window onload event. The results of this variation are shown in Table 3 (raw results in Browserscope).

Table 3. maximum localStorage, delayed getItem
User Agent size (K-chars) delta getItem (ms) response- Start time response- End time dom- Loading time BEFORE getItem time AFTER getItem time dom- Interactive time
Chrome 33 5,120 1,026 3 5 21 1112 2139 85
Chrome Mobile 32 5,120 1,066 83 87 188 1240 2294 234
Firefox 27 5,120 0 3 17 4 1038 1039 20
IE 11 4,883 872 5 5 5 1075 1967 49
Opera 19 5,120 313 2 4 15 1025 1336 23
Mobile Safari 7 2,560 104 1003 1106
Safari 7 2,560 177 1004 1181

Table 3 confirms that Firefox is precaching localStorage – “delta getItem” is 0 ms because there was plenty of time for Firefox to finish precaching before the call to getItem(). All the other browsers, however, have positive values for “delta getItem”. The values for Chrome, Chrome Mobile, and IE are comparable between Table 1 and Table 3: 1038 vs 1026, 1114 vs 1066, and 759 vs 872.

The values for Opera, Mobile Safari, and Safari are slower in Table 1 compared to Table 3: 930 vs 313, 453 vs 104, and 520 vs 177. I don’t have an explanation for this. I don’t think these browsers are precaching localStorage (the values from Table 3 would be closer to zero). Perhaps the call to getItem() took longer in Table 1 because the page was actively loading and there was contention for memory and CPU resources, whereas for Table 3 the page had already finished loading.

500K localStorage

So far we’ve measured maximum localStorage (Table 1) and 10K of localStorage (Table 2). Table 4 shows the results with 500K of localStorage. All of the “delta getItem” values fall between the 10K and maximum values. No real surprizes here.

Table 4. 500K localStorage
User Agent size (K-chars) delta getItem (ms) response- Start time response- End time dom- Loading time BEFORE getItem time AFTER getItem time dom- Interactive time
Chrome 33 500 20 3 4 19 25 43 43
Chrome Mobile 32 500 164 78 85 144 183 368 368
Firefox 27 500 14 2 30 3 15 30 31
IE 11 500 32 5 5 5 48 89 83
Opera 19 500 36 2 4 14 23 57 58
Mobile Safari 7 500 37 1 38
Safari 7 500 44 0 44

Conclusions

The goal of this blog post was to see if Firefox’s localStorage precaching behavior was measurable with Navigation Timing. We succeeded in doing that in this contrived example. For real world pages it might be harder to capture Firefox’s behavior. If localStorage is accessed early in the page then it may recreate the condition found in this test where responseEnd is blocked waiting for precaching to complete.

Another finding from these tests is that Firefox is the only browser doing precaching. This means that the simple approach of wrapping the first access to localStorage with timers accurately captures localStorage performance in all browsers except Firefox.

It’s hard not to focus on the time values from these tests but keep in mind that this is a small sample size. I did nine tests per browser for Table 1 and dropped to five tests per browser for Tables 2-4 to save time. Another important factor is that the structure of my test page is very simple and unlike almost any real world website. Rather than focus on these time values, it would be better to use the conclusions about how localStorage performs to collect real user metrics.

There are takeaways here for browser developers. There’s quite a variance in results across browsers, and Firefox’s precaching behavior appears to improve performance. If browser teams do more extensive testing coupled with their knowledge of current implementation it’s likely that localStorage performance will improve.

Update Feb 24, 2014: This Firefox telemetry chart shows that 80% of first accesses to localStorage have zero wait time because localStorage is already in memory confirming that precaching localStorage has a positive effect on performance.

A smaller takeaway is the variance in storage size. Whether you measure by number of characters or bytes, Safari holds half as much as other major browsers.

Notes, Next Steps, and Caveats

As mentioned above, the time values shown in these results are based on a small sample size and aren’t the focus of this post. A good next step would be for website owners to use these techniques to measure the performance of localStorage for their real users.

In constructing my test page I tried various techniques for filling localStorage. I settled on writing as many strings as possible of length 1M, then 100K, then 10K, then 1K – this resulted in a small number of keys with some really long strings. I also tried starting with strings of length 100K then dropping to 1K – this resulted in more keys and shorter strings. I found that the first approach (with some 1M strings) produced slower read times. A good follow-on experiment would be to measure the performance of localStorage with various numbers of keys and string lengths.

With regard to how browsers encode characters when saving to localStorage, I chose to use string values that contained some non-ASCII characters in an attempt to force browsers to use the same character encoding.

In my call to getItem() I referenced a key that did not exist. This was to eliminate any variability in reading (potentially large) strings into memory since my main focus was on reading all of localStorage into memory. Another follow-on experiment would be to test the performance of reading keys of different states and lengths to see if browsers performed differently. For example, one possible browser optimization would be to precache the keys without the values – this would allow for more efficient handling of the case of referencing a nonexistant key.

A downside of Firefox’s precaching behavior would seem to be that all pages on the domain would suffer the penalty of reading localStorage regardless of whether they actually used it. However, in my testing it seemed like Firefox learned which pages used localStorage and avoided precaching on pages that didn’t. Further testing is needed to confirm this behavior. Regardless, it seems like a good optimization.

Thanks

Thanks to Nicholas Zakas, Jonas Sicking, Honza Bambas, Andrew Betts, and Tony Gentilcore for providing advice and information for this post.

3 Responses to Measuring localStorage Performance

  1. Another important thing here is that you were measuring on a device with SSD disk. Many times with IO, latency is a bigger problem than bandwidth. I.e. it often takes a longer time for the disk to find the location to read from, than doing the actual read.

    One reason for this is that we need to go through various filesystem data structures (which might be located in different physical locations on the disk) as well as file format structures, before we can start actually reading data. Each such jump can be expensive.

    This is especially true on magnetic media this is even worse. The head of the disk needs to be physically moved to the right location and the disk needs to be spun to the right angle.

    In SSD this is much less the case since there’s no moving parts. Seek times are dramatically faster as all you need to do is to switch a few transistors into the right mode.

    The point of all this is that you might see much bigger numbers on a more typical user machine.

    IO varies dramatically from computer to computer. Things like disk type, disk rpm, disk fragmentation and what other software is currently doing IO has dramatically large effects on IO latency and bandwidth. And it is obviously going to vary from user to user.

    It would be super interesting to get realworld data here. I.e. grab a realworld website that doesn’t use localStorage and create three groups: Some that still doesn’t get any localStorage data, some that get small amounts and some that gets large amounts.

    Then use navigation timing and timing getItem to see what the effects are on average and first-visit-of-day times are for the three groups. No need to ask users to shut down the browser and/or OS, if browser and OS caching speeds things up enough to remove the problems, then that’s great.

    Mozilla is gathering telemetry data and making it publicly available here

    http://telemetry.mozilla.org/#release/26/LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS

    Make sure to switch to calendar dates for the data to make sense.

  2. Jonas: Thanks for the links to Mozilla’s telemetry data. Another compelling chart is this one that shows 80% of first accesses to localStorage didn’t incur any wait. I updated the post to point this out.

    I totally agree that getting more real user data will help quantify the impact localStorage has on page load times. I hope some sites will do that and share their results.

  3. Why doesn’t Google Chrome precache localStorage just like Firefox?

    BTW: DOM loading times are much shorter in Firefox and IE than in Chrome. Is it due to localStorage delay or something else?