KV Storage - the Web's First Built-in Module

Browser vendors and web performance experts have been saying for the better part of the last decade that localStorage is slow, and web developers should stop using it.

To be fair, the people saying this are not wrong. localStorage is a synchronous API that blocks the main thread, and any time you access it you potentially prevent your page from being interactive.

The problem is the localStorage API is just so temptingly simple, and the only asynchronous alternative to localStorage is IndexedDB, which (let's face it) is not known for its ease of use or welcoming API.

So developers are left with a choice between something hard to use and something bad for performance. And while there are libraries that offer the simplicity of the localStorage API while actually using asynchronous storage APIs under the hood, including one of those libraries in your app has a file-size cost and can eat into your performance budget.

But what if it were possible to get the performance of an asynchronous storage API with the simplicity of the localStorage API, without having to pay the file size cost?

Well, soon there may be. Chrome is experimenting with a new feature known as built-in modules, and the first one we're planning to ship is an asynchronous key/value storage module called KV Storage.

But before I get into the details of the KV Storage module, let me explain what I mean by built-in modules.

What are built-in modules?

Built-in modules are just like regular JavaScript modules, except that they don't have to be downloaded because they ship with the browser.

Like traditional web APIs, built-in modules must go through a standardization process — each will have its own specification that requires a design review and positive signs of support from both web developers and other browser vendors before it can ship. (In Chrome, built-in modules will follow the same launch process we use to implement and ship all new APIs.)

Unlike traditional web APIs, built-in modules are not exposed on the global scope — they're only available via imports.

Not exposing built-in modules globally has a lot of advantages: they won't add any overhead to starting up a new JavaScript runtime context (e.g. a new tab, worker, or service worker), and they won't consume any memory or CPU unless they're actually imported. Furthermore, they don't run the risk of naming collisions with other variables defined in your code.

To import a built-in module, you use the prefix std: followed by the built-in module's identifier. For example, in supported browsers, you could import the KV Storage module with the following code (see below for how to use a KV Storage polyfill in unsupported browsers):

import storage, {StorageArea} from 'std:kv-storage';

The KV Storage module

The KV Storage module is similar in its simplicity to the localStorage API, but its API shape is actually closer to a JavaScript Map. Instead of getItem(), setItem(), and removeItem(), it has get(), set(), and delete(). It also has other map-like methods not available to localStorage, like keys(), values(), and entries(), and like Map, its keys do not have to be strings. They can be any structured-serializable type.

Unlike Map, all KV Storage methods return either promises or async iterators (since the main point of this module is that it's not synchronous, in contrast to localStorage). To see the full API in detail, you can refer to the specification.

As you may have noticed from the code example above, the KV Storage module has one default export storage and one named export StorageArea.

storage is an instance of the StorageArea class with the name 'default', and it's what developers will use most often in their application code. The StorageArea class is provided for cases where additional isolation is needed (e.g. a third-party library that stores data and wants to avoid conflicts with data stored via the default storage instance). StorageArea data is stored in an IndexedDB database with the name kv-storage:${name}, where name is the name of the StorageArea instance.

Here's an example of how to use the KV Storage module in your code:

import storage from 'std:kv-storage';

const main = async () => {
  const oldPreferences = await storage.get('preferences');

  document.querySelector('form').addEventListener('submit', async () => {
    const newPreferences = Object.assign({}, oldPreferences, {
      // Updated preferences go here...
    });

    await storage.set('preferences', newPreferences);
  });
};

main();

What if a browser doesn't support a built-in module?

If you're familiar with using native JavaScript modules in browsers, you probably know that (at least up until now) importing anything other than a URL will generate an error. And std:kv-storage is not a valid URL.

So that raises the question: do we have to wait until all browsers support built-in modules before we can use it in our code? Thankfully, the answer is no!

You can actually use built-in modules as soon as even one browser supports them thanks to the help of another feature we're experimenting with called import maps.

Import maps

Import maps are essentially a mechanism by which developers can alias import identifiers to one or more alternate identifiers.

This is powerful because it gives you a way to change (at runtime) how a browser resolves a particular import identifier across your entire application.

In the case of built-in modules, this allows you to reference a polyfill of the module in your application code, but a browser that supports the built-in module can load that version instead!

Here's how you would declare an import map to make this work with the KV Storage module:

<!-- The import map is inlined into your page -->
<script type="importmap">
{
  "imports": {
    "/path/to/kv-storage-polyfill.mjs": [
      "std:kv-storage",
      "/path/to/kv-storage-polyfill.mjs"
    ]
  }
}
</script>

<!-- Then any module scripts with import statements use the above map -->
<script type="module">
  import storage from '/path/to/kv-storage-polyfill.mjs';

  // Use `storage` ...
</script>

The key point in the above code is that the URL /path/to/kv-storage-polyfill.mjs is being mapped to two different resources: std:kv-storage and then the original URL again, /path/to/kv-storage-polyfill.mjs.

So when the browser encounters an import statement referencing that URL (/path/to/kv-storage-polyfill.mjs), it first tries to load std:kv-storage, and if it can't, then it falls back to loading /path/to/kv-storage-polyfill.mjs.

Again, the magic here is that the browser doesn't need to support import maps or built-in modules for this technique to work since the URL being passed to the import statement is the URL for the polyfill. The polyfill is not actually a fallback, it's the default. The built-in module is a progressive enhancement!

What about browsers that don't support modules at all?

In order to use import maps to conditionally load built-in modules, you have to actually use import statements, which also means you have to use module scripts, i.e. <script type="module">.

Currently, more than 80% of browsers support modules, and for browsers that don't, you can use the module/nomodule technique to serve a legacy bundle. Note that when generating your nomodule build, you'll need to include all polyfills because you know for sure that browsers that don't support modules will definitely not support built-in modules.

KV Storage demo

To illustrate that it's possible to use built-in modules while still supporting older browsers, I've put together a demo that incorporates all the techniques described above and runs in all browsers today:

  • Browsers that support modules, import maps, and the built-in module do not load any unneeded code.
  • Browsers that support modules and import maps but do not support the built-in module load the KV Storage polyfill (via the browser's module loader).
  • Browsers that support modules but do not support import maps also load the KV Storage polyfill (via the browser's module loader).
  • Browsers that do not support modules at all get the KV Storage polyfill in their legacy bundle (loaded via <script nomodule>).

The demo is hosted on Glitch, so you can view its source. I also have a detailed explanation of the implementation in the README. Feel free to take a look if you're curious to see how it's built.

In order to actually see the native built-in module in action, you have to load the demo in Chrome 74 or later with the experimental web platform features flag turned on (chrome://flags/#enable-experimental-web-platform-features).

You can verify that the built-in module is being loaded because you won't see the polyfill script in the source panel in DevTools; instead, you'll see the built-in module version (fun fact: you can actually inspect the module's source code or even put breakpoints in it!):

The KV Storage module source in Chrome DevTools

Please give us feedback

This introduction should have given you a taste of what may be possible with built-in modules. And hopefully, you're excited! We'd really love for developers to try out the KV Storage module (as well as all the new features discussed here) and give us feedback.

Here are the GitHub links where you can give us feedback for each of the features mentioned in this article:

If your site currently uses localStorage, you should try switching to the KV Storage API to see if it meets all your needs. And if you sign up for the KV Storage origin trial, you can actually deploy these features today. All your users should benefit from better storage performance, and Chrome 74+ users won't have to pay any extra download cost.