Import Maps Release &
Module CDN Launch
Guy Bedford, March 2nd 2021
Today Chrome 89 ships as stable with support for package import maps in browsers. Aligning with this support, a new ga.jspm.io
CDN is being launched today as a native modules import map CDN along with an online import map generator for creating import maps against the module CDN.
Import Maps Explainer
Import maps allow defining the locations of modules imported from JavaScript in the browser, effectively like you would expect in any other language (and as is already supported in Node.js):
<script type="module">
import pkg from 'pkg';
</script>
Before import maps, if you run the above in any browser you would get the following error:
'Uncaught TypeError: Failed to resolve module specifier "pkg". Relative references must start with either "/", "./", or "../".'
This is because unlike other web resources, the JS modules specification for HTML reserved the space of these non-relative references (called "bare specifiers") exactly to allow custom package imports via import maps.
To map this specifier with an import map, we add the new "importmap"
script type to the web page:
<script type="importmap">
{
"imports": {
"pkg": "./pkg/main.js"
}
}
</script>
And as of today, the above workflow is supported natively in Chromium for the first time.
In addition to the "imports"
field in the import map, there is also the "scopes"
field which allows for scoping the import mapping (useful when there are naming / version conflicts). Import maps can even map entire resolved URLs which can be useful in mocking workflows.
The JavaScript Module Caching Tradeoff
Import maps may seem nice and all, but perhaps they don't actually seem like much of a big deal - do they really solve any production-time technical problems?
The answer is that they do actually solve quite a deep performance problem with JS modules in browsers!
Without import maps, there is a natural caching tradeoff that applies to shipping JS modules in production, something like the following:
- On the web you usually want all URLs to be unique and cached with far-future expires (the fastest request being no request).
- We usually achieve this with unique URL schemes by including hashes or a unique build identifier in URLs so that updates don't cause conflicts.
- Since modules have to import eachother by name
app-a09s8df0.js
will end up containingimport './dependency-s8df79sd.js'
, and as a result the top-level hash is dependent on the lower-level hashes. A change to a deep dependency changes theimport './dependency-s8df79sd.js'
into aimport './dependency-qw97g23s.js'
, which in turn changes the hash ofapp-a09s8df0
itself because its contents have changed. That is, a deep dependency change invalidates all the parent modules. A small change causes a large invalidation. - So if we want to be able to have cache sharing of parent modules while invalidating dependencies, we may be better off having
app.js
loaddependency.js
and then just not using far-future expires.
There is thus a complex tradeoff to be made between perfect caching that quickly invalidates with updates, and imperfect caching that might provide better cache sharing for updates.
In comparison to build techniques today the above might not sound so bad, but we shouldn't compare to what we do today - we should compare to the theoretical best case.
Ideal Modular Caching
The theoretical ideal for module caching would be for each module to be cached with far future expires, while only being invalidated when that module itself changes.
Import maps can provide us with exactly this property:
<script type="importmap">
{
"imports": {
"app": "./dist/app-cvf98b7c.js"
"dependency": "./dist/dependency-s9df7987.js"
}
}
</script>
<script type="module">import 'app'</script>
Where app.js
would contain:
import 'dependency';
We now have the ability to independently update either app
or dependency
using the import map, while having both modules cached with far-future expires.
Import maps bring the possibility of perfect caching for incremental updates of applications on the web.
It turns out that coupled with using dynamic import()
lazy loading for first-load optimization, JS modules can be a pretty good bet for production workflow performance.
Module CDNs
Module CDNs take the perfect caching concept and extend it based on the following principle - taking the JS package as both the unit of optimization and the unit of perfect caching.
For a given package and version of that package, we optimize the entire package unit as a whole and host it under a unique URL with the version number.
Import maps applied to this model then allow perfect caching at the level of per-package granularity. The benefit of this being that the optimized package files can be shared between any number of applications since packages are the granularity of usage. In this way it acts as a source of precompiled and optimized JS packages for the browser.
The new ga.jspm.io CDN is being launched to serve this role, with import maps generated by the Online Import Map Generator.
This effectively then provides something akin to the npm install workflow that can work natively for browsers, with the import map treated as a sort of lockfile. JSPM as a browser native package manager stems exactly from this principle.
Support in Other Browsers
While we wait the years it will take for import maps to be widely supported for browsers other than Chrome 89, there are two production-suitable approaches currently available:
- ES Module Shims: A fast Wasm-based polyfill that just replaces the specifiers with their full URLs while still using the native loader. It's good enough for production in most cases and is used by the JSPM generator web application itself.
- SystemJS: A complete variant of the
ga.jspm.io
CDN is available serving SystemJS modules atga.system.jspm.io
, available for SystemJS import maps when selecting the "SystemJS Import Map" option from the JSPM Generator. SystemJS provides a workflow where native module semantics can be replicated in all browsers using the SystemJS module fomat.
Try it Out
I had a lot of fun building the JSPM Generator using this approach and not having to touch a CLI apart from running a local server in dev and leaving it alone - and I really hope this can make JS development more fun for you too.
Please feel free to ask questions or share feedback - this stuff only works in collaboration. And there is still a lot of work to do. If you're interested in getting involved in the project please get in touch too.
Follow the workflows here or try out the import map generator directly.
For questions or further discussion about jspm, join jspm on Discord. For CDN issues, post to the project issue tracker.