Bare Import Specifiers and Import Maps in JavaScript
This article explains what bare import specifiers are, why native browser JavaScript environments cannot resolve them by default, and how import maps bridge this gap. Modern web browsers natively support ECMAScript modules (ESM), but their resolution mechanism differs fundamentally from server-side runtimes like Node.js. By understanding bare specifiers and implementing import maps, developers can use clean, package-style import syntax directly in the browser without relying on build tools or bundlers.
What Are Import Specifiers?
An import specifier is the string literal that follows the
from keyword in a JavaScript import statement.
In native ECMAScript modules, specifiers generally fall into three
categories:
- Relative specifiers: Start with
./or../(e.g.,import { utils } from './utils.js';). - Absolute specifiers: Start with
/or a full URL protocol (e.g.,import { api } from '/scripts/api.js';orimport { lib } from 'https://cdn.example.com/lib.js';). - Bare specifiers: Strings that contain no path
indicator or protocol (e.g.,
import React from 'react';orimport dayjs from 'dayjs';).
A bare import specifier does not explicitly tell the runtime where to locate the file on the network or filesystem; it only provides the name of a module.
Why Browsers Cannot Resolve Bare Specifiers
In Node.js, bare import specifiers are resolved using a file-system
lookup algorithm that automatically searches through nested
node_modules directories. This process involves multiple
synchronous file system checks until the target file is found.
Native web browsers do not have direct access to a local filesystem
and must fetch resources asynchronously over HTTP/HTTPS. If browsers
attempted to mimic Node.js resolution, they would have to guess file
paths and make dozens of speculative network requests for every import.
This would result in severe latency, network waterfalls, and failed
requests. Consequently, the native browser ES module specification
strictly requires explicit URLs or relative paths for all module imports
and throws a TypeError whenever it encounters an unmapped
bare specifier.
How Import Maps Solve the Problem
An import map is a JSON structure embedded directly in an HTML
document using a <script type="importmap"> tag. It
instructs the browser’s module loader on how to resolve specific module
specifiers to real URLs.
Example of an Import Map
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js",
"components/": "/src/components/"
}
}
</script>
<script type="module">
// The browser uses the import map to resolve 'lodash' to the CDN URL
import debounce from 'lodash';
// Resolves 'components/button.js' to '/src/components/button.js'
import { Button } from 'components/button.js';
</script>Key Benefits of Import Maps
- Bundler-Free Development: Developers can write modern module code with clean import paths directly in the browser without needing Webpack, Rollup, or Vite during simple prototyping.
- Centralized Version Management: Package URLs are defined in a single location in the HTML rather than hardcoded throughout individual JavaScript files.
- Scoping Support: Import maps allow defining scoped mappings, enabling different modules within the same application to use different versions of the same dependency.