Import Maps and Native ES Modules: Performance Implications
AI generated
60fps
ms
Web Performance
Import Maps and Native ES Modules: Performance Implications
When skipping the bundler actually pays off

Import Maps solve a long-standing problem: clean, readable module specifiers in native ES modules without a bundler step. But the more interesting question isn't the syntax, it's performance: when does natively loading many separate module files actually beat a bundled JavaScript package, and when doesn't it.

15 min read Import Maps Native ES Modules

1. The problem with bare module specifiers

Native ES modules in the browser natively understand only relative or absolute URLs as import paths. An import like import { debounce } from 'lodash-es' simply doesn't work in the browser without additional configuration, since the browser has no idea where the lodash-es package lives, unlike Node.js or a bundler, which have their own module resolution based on node_modules. These so-called bare specifiers were for a long time a central argument for using a bundler in every project, even when its other features weren't needed at all.

Import Maps solve exactly this problem by giving the browser an explicit mapping from module specifiers to actual URLs. That makes the same readable, short import style familiar from a Node.js environment usable in the browser too, without a build step having to rewrite the import paths beforehand. This is especially interesting for projects that deliberately want to avoid a complex build process, for instance for the sake of simplicity or to eliminate build times entirely.

2. Import Maps: syntax and how they work

An import map is either embedded directly in the HTML as its own script element of type importmap, or referenced through an external JSON file. At its core it consists of an imports object mapping module specifiers to URLs, and it can additionally define so-called scopes, which allow different mappings for different parts of the application, for example when two dependencies need different versions of the same library.

It matters that the import map must appear before any module script in the document, since the browser needs to know it already while parsing module imports. A document that has already loaded and evaluated also can't change its import map afterward, which provides predictability but also means the mapping has to be fully settled from the start.


<script type="importmap">
{
  "imports": {
    "lodash-es": "/vendor/lodash-es/lodash.js",
    "chart-utils": "/js/modules/chart-utils.js",
    "@app/": "/js/app/"
  }
}
</script>

<script type="module">
  import { debounce } from 'lodash-es';
  import { formatCurrency } from '@app/format.js';

  const search = document.querySelector('#search');
  search.addEventListener('input', debounce(handleSearch, 250));
</script>

3. HTTP/2 multiplexing as a prerequisite

Native ESM loading means in practice that the browser issues a separate HTTP request for every imported module, which can quickly add up to dozens or even hundreds of individual requests in a deeply nested dependency chain. Under HTTP/1.1, where only a limited number of parallel requests is possible per connection, that would be a substantial performance disadvantage compared to a single bundled JavaScript package.

Only HTTP/2 (and even more so HTTP/3) makes native ESM loading practically usable at performance, since both protocols allow true multiplexing over a single connection: many requests can be handled in parallel over the same TCP connection (over QUIC for HTTP/3), without the classic overhead of separate connections or the head-of-line blocking problem inherent to HTTP/1.1. Without HTTP/2 or HTTP/3, native ESM loading with many modules is practically always the slower option.

4. How the browser resolves the module graph

When the browser encounters a module script, it first builds the full dependency graph by recursively following all import statements, before a single module is actually executed. This process runs in parallel and asynchronously: as soon as a module is discovered, the browser immediately starts downloading it, without waiting for previous downloads to finish, which substantially softens the impact of many individual requests under HTTP/2.

Only once the entire graph has been resolved and downloaded does actual execution begin, in the correct order dictated by the dependencies. This two-stage architecture of resolution and execution differs fundamentally from a classic bundle, where resolution and bundling already happen at build time, and the browser only ever has to load a single, already-sorted file at runtime.

5. Native ESM loading vs. a bundled approach

A single bundled JavaScript package has the obvious advantage that only one request is needed, which minimizes connection setup and protocol overhead. At the same time, a bundle also means that even a small change to a single line of code affects the cache key of the entire file, so users have to re-download the whole bundle after a deployment, even if 99 percent of the code didn't change at all.

Native ESM loading with import maps flips that relationship around: many small files mean more requests, but also considerably more granular caching, since a change to a single module only affects that module's own cache entry, while every unchanged module can still be served from the browser cache. For applications with frequent, small deployments, this effect can more than offset the additional requests.

6. When native ESM loading actually performs better

Native ESM loading shows its strengths especially in applications with a manageable number of modules, frequent incremental deployments, and a user base on modern devices with stable HTTP/2 or HTTP/3 connections. In such cases, you benefit from granular caching without being noticeably slowed down by the additional requests, since the protocol's multiplexing largely neutralizes their sheer number.

Native ESM loading is also simply the better choice for development environments, regardless of the production strategy, which is exactly why modern dev servers like Vite rely on native ESM in dev mode: the lack of a build step on every code change leads to a considerably faster feedback loop for developers, while a bundled build can still be produced for production.

7. When a bundled approach still makes sense

For applications with a very large number of small modules, say several hundred individual files, the advantage of a bundled approach often still wins out, even under HTTP/2, since every individual request still carries a certain constant overhead from header processing and JavaScript parsing per file, despite multiplexing. For users on slower or less stable connections, for example mobile users with frequent connection drops, a single robust file can also be more reliable than many individual requests.

Bundlers also frequently offer valuable additional features such as tree-shaking, which automatically removes unused code, or minification, which is either unavailable or requires extra tooling with native ES modules that skip the build step. For very large applications with many unused code paths, this effect can outweigh the caching advantage of individual modules.

8. Caching benefits of granular modules in practice

modulepreload links can also compensate for part of the downside of many individual requests, since they instruct the browser to download critical modules early and keep them ready within the module graph, similar to preload links for other resource types. Combined with long-term cache headers and content-based filenames (content hashing), this makes it possible to build a system where returning users only need to re-download the modules that actually changed after a deployment.

This granular caching advantage shows up especially clearly in applications with a stable, rarely changed core library and more frequently updated application code: while a single bundle would re-deliver the stable library on every change to the application code, it stays untouched in the user's cache with native modules.

9. A practical recommendation for production use

For most production environments with a larger number of modules, a bundled build remains the more pragmatic and generally more performant choice, combined with code splitting for rarely needed areas to at least capture some of the granular caching advantage. Import Maps and native ESM loading, on the other hand, are an excellent fit for smaller applications, internal tools, prototypes, or projects where deliberately skipping a build step is desired for the sake of simplicity.

In every case, the decision should rest on actual measurement, for instance a direct comparison of load times with and without bundling under realistic network conditions, rather than blanket adherence to either strategy. Since both browser implementations and protocol support keep evolving, the balance between the two approaches could well continue shifting further in favor of native loading over time.

Criterion Native ESM with Import Maps Bundled build
Number of requests One request per module One or a few requests total
Caching after deployment Only changed modules re-download Whole bundle often re-downloads
Prerequisite HTTP/2 or HTTP/3 essentially required Also works under HTTP/1.1
Tree-shaking and minification Not without extra tooling Provided by default via the bundler
Best suited for Smaller apps, tools, development Large applications with many modules

Mironsoft

Web performance, Core Web Vitals, and load time optimization

Load times that don't make users bounce before the page is even visible?

We review existing websites for slow Core Web Vitals, bloated JavaScript bundles, and unnecessary render blockers, then build a performance foundation that stays measurable instead of just looking good once.

Performance Audit

Systematically measuring and fixing Core Web Vitals, load waterfall, and render blockers.

Bundle Optimization

Specifically reducing JavaScript and CSS bundle size and improving code splitting.

Monitoring Setup

Establishing continuous performance monitoring instead of a one-time snapshot.

10. Summary

Import Maps

Goal

Clean module specifiers in native ES modules without a bundler

Requirement

HTTP/2 or HTTP/3 multiplexing for acceptable performance

Benefit

Granular caching of individual, unchanged modules

Limit

With very many modules, bundling usually stays faster

11. FAQ: Import Maps

1What is an import map?
A JSON-based configuration object, usually embedded as a script element of type importmap, that maps bare module specifiers like 'lodash-es' to actual URLs so native ES modules work in the browser without a bundler.
2Do I need a bundler to use import maps?
No, that's exactly the point: import maps allow readable module specifiers even without a build step. A bundler can still be useful, but it's no longer strictly required for things to function.
3Why is HTTP/2 so important for native ESM loading?
Because native ESM loading generates many individual requests. Only HTTP/2 and HTTP/3 allow true multiplexing over a single connection, letting all those requests be handled in parallel without the overhead of separate connections.
4Is native ESM loading always faster than a bundle?
No, it depends heavily on context. With few modules and frequent small deployments it can be faster, but with very many modules a bundled build generally stays the quicker option.
5What's the main caching advantage of native modules?
If only a single module changes, only its cache entry needs to be refreshed, whereas with a single bundle, even a small change forces the whole package to be redelivered.
6Can I combine import maps with modulepreload?
Yes, and it's recommended. modulepreload links tell the browser to download critical modules early, which compensates for part of the downside of many individual requests.
7Do all browsers support import maps?
All current versions of the major browsers now support import maps. For very old browsers without support, a polyfill or a classic bundled approach is still needed.
8Do native ES modules lose tree-shaking and minification?
Without extra tooling, yes, since those optimizations are traditionally part of the bundling process. Projects that skip a build step lose these benefits accordingly.
9What kind of projects are import maps especially well suited for?
Smaller applications, internal tools, prototypes, and projects that deliberately want to avoid a complex build process, as well as development environments that need a fast feedback loop.
10Should I skip bundling for a large production application?
Generally not advisable. With many modules, the advantage of a bundled build usually still wins out, combined with code splitting for rarely needed areas of the application.