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, now there is. Chrome is experimenting with a new feature called 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 and have well-defined specifications, but unlike traditional web APIs, they're 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 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
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);
});
};
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 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 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 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 to older browsers. 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 today while still supporting older browsers, I've put together a demo that incorporates all the techniques described above:
- 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 (currently Chrome Dev or Canary) 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!):
Please give us feedback
This introduction should have given you a taste of what's 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, and if you sign up for the KV Storage origin
trial, you can
actually deploy your changes today! All your users should benefit from better
performance, and Chrome 74+ users won't have to pay any extra download cost.