I'd like to write to localStorage often. I'd also like to write to it synchronously in response to user interaction.

Past wisdom says this is a bad idea and will give me Angry Users Syndrome very quickly. Bad ideas are fun though, so let's see how much of a bad idea it is.

TL;DR: It's not a bad idea for (at least) the latest Firefox, WebKit, and Chromium browsers. localStorage.setItem and localStorage.getItem access fast in-memory data structures and updating the disk is done asynchronously in the background.

To get an idea of what we're looking at, let's take a quick dip into Firefox's source code (you can follow along on GitHub). I'm starting from the top of the C++ implementation of localstorage.setItem which is at DOMStorage::SetItem in dom/storage/DOMStorage.cpp. There's nothing interesting in that function, but one layer deeper at DOMStorageCache::SetItem in dom/storage/DOMStorageCache.cpp we get something interesting.

Below is the entirety of the function with my own comments. Sorry in advance for shoving a bunch of C++ at you…

// Firefox loads all existing localStorage data from the disk the first
// it hits your site. If that operation hasn't finished yet, we get to
// twiddle our thumbs for a bit.
if (Persist(aStorage)) {
  WaitForPreload(Telemetry::LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS);
  if (NS_FAILED(mLoadResult)) {
    return mLoadResult;
  }
}

// data is an in-memory hash table containing all of our localStorage
// data, so this is blazing fast.
Data& data = DataSet(aStorage);
if (!data.mKeys.Get(aKey, &aOld)) {
  SetDOMStringToNull(aOld);
}

// Make sure the quota isn't going to be exeeded. Nothing terribly
// interesting here.
const int64_t delta = static_cast<int64_t>(aValue.Length()) -
                      static_cast<int64_t>(aOld.Length());
if (!ProcessUsageDelta(aStorage, delta)) {
  return NS_ERROR_DOM_QUOTA_REACHED;
}

if (aValue == aOld && DOMStringIsNull(aValue) == DOMStringIsNull(aOld)) {
  return NS_SUCCESS_DOM_NO_OPERATION;
}

// Update the in-memory hash table.
data.mKeys.Put(aKey, aValue);

if (Persist(aStorage)) {
  if (!sDatabase) {
    NS_ERROR("Writing to localStorage after the database has been shut down"
             ", data lose!");
    return NS_ERROR_NOT_INITIALIZED;
  }

  // Asynchonously update the disk.
  if (DOMStringIsNull(aOld)) {
    return sDatabase->AsyncAddItem(this, aKey, aValue);
  }

  return sDatabase->AsyncUpdateItem(this, aKey, aValue);
}

return NS_OK;

The corresponding localStorage.getItem implementation is even simpler and just accesses that hash table (it's in the same file if you want to take a look).

So to summarize: whenever we access localStorage we're accessing a hash table and we're not going to block on the disk, unless we haven't finished the initial preload.

This is nice news and I like this, but there's quite a few browsers out there. Let's take a look at Chromium next (follow along on Google Code).

WebStorageAreaImpl::setItem at src/content/renderer/dom_storage/webstoragearea_impl.cc is the top of the C++ implementation, but again it doesn't have anything interesting. We need to dig down to DOMStorageCachedArea::SetItem in src/content/renderer/dom_storage/dom_storage_cached_area.cc in order to see something cool (comments by me again):

// A quick check to reject obviously overbudget items to avoid
// the priming the cache.
if (key.length() + value.length() > kPerStorageAreaQuota)
  return false;

// Similarily to Firefox, we load everything on disk into memory. It
// doesn't look like preloading is automatically triggered when users hit
// your site though, and must be manually initiated by accessing local
// storage.
PrimeIfNeeded(connection_id);

// map_ is (basically) a std::map object, which is going to be some data
// structure that lets you do this operation in O(log n) time.
base::NullableString16 unused;
if (!map_->SetItem(key, value, &unused))
  return false;

// Ignore mutations to 'key' until OnSetItemComplete.
ignore_key_mutations_[key]++;

// Asynchronously update the disk.
proxy_->SetItem(
    connection_id, key, value, page_url,
    base::Bind(&DOMStorageCachedArea::OnSetItemComplete,
               weak_factory_.GetWeakPtr(), key));
return true;

The corresponding localStorage.getItem implementation just accesses the std::map object.

To summarize: Chromium behaves the same as Firefox except that it does not seem to preload the cache until you hit local storage for the first time.

WebKit seems like a good next target (github). Storage::setItem in Source/WebCore/storage/Storage.cpp is the top of the implementation, and it does have something interesting for once. We can see that private browsing mode kills localStorage. Burying deeper down though we see a similar story at StorageAreaMap::setItem in Source/WebKit2/WebProcess/Storage/StorageAreaMap.cpp.

I'm not going to paste the code here because it's basically the same as the above two. There's the wait on the preload, then an access to an in-memory data structure (a hash table this time), and then an asynchronous call to update the disk.

It would be a good idea to go back through the git history of the three repositories I've been looking at to see when they became super fast. I'd also like to do some testing to Internet Explorer to try and figure out if its implementation is also fast. I'll leave that to another post though.


--

« more posts | follow me via twitter, or RSS