Browser vendors and web performance experts have been saying for the better part
of the last decade that localStorage is
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
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

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, now there is. Chrome is experimenting with a new feature called built-in
, 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
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 and have well-defined specifications, but unlike traditional web APIs,
they’re not exposed on the global scope—they’re only available via

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
, you could import the KV Storage module with the following code
(see below for how to use a KV Storage polyfill in unsupported

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

The KV Storage module

The KV Storage module is similar in its simplicity to the localStorage
, but
its API shape is actually closer to a
JavaScript Map.
Instead of getItem(),
and removeItem(),
it has get(),
and delete().
It also has other map-like methods not available to localStorage, like
values(), and
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 it’s not synchronous, in contrast to
localStorage). To see the full API in detail, you can refer to the

As you may have noticed from the code example above, the KV Storage module has
two named exports: storage and 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', () => {
    const newPreferences = Object.assign({}, oldPreferences, {
      // Updated preferences go here...

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


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 module before we can use it in our code?

Thankfully, the answer is no! You can actually use built-in modules in your
code today, with the help of another new feature called
import maps.

Import maps

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

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

The key point in the above code is 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

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
, i.e.