Bundling
Import Maps
Control module resolution in the browser
Intermediate
bundling modules browser native
Definition
Import Maps allow you to control how the browser resolves module specifiers in ES modules. They enable bare module imports (like import React from 'react') to work natively in browsers without bundlers, mapping these specifiers to actual URLs.
The Problem
Without Import Maps
<!-- Must use full URLs -->
<script type="module">
import React from 'https://cdn.skypack.dev/react';
import ReactDOM from 'https://cdn.skypack.dev/react-dom';
import { useState } from 'https://cdn.skypack.dev/react';
// Or relative paths
import { utils } from './utils.js';
import { helpers } from '../lib/helpers.js';
</script>
With Import Maps
<script type="importmap">
{
"imports": {
"react": "https://cdn.skypack.dev/react",
"react-dom": "https://cdn.skypack.dev/react-dom",
"utils/": "./utils/"
}
}
</script>
<script type="module">
import React from 'react';
import ReactDOM from 'react-dom';
import { formatDate } from 'utils/dates.js';
// Clean, works in browser without bundler!
</script>
Basic Syntax
Simple Mapping
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js",
"lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4/lodash.js",
"app": "./src/app.js",
"components/": "./src/components/"
}
}
</script>
Scoped Imports
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
},
"scopes": {
"/legacy/": {
"vue": "https://unpkg.com/vue@2/dist/vue.esm-browser.js"
}
}
}
</script>
<!-- /legacy/app.js uses Vue 2 -->
<script type="module" src="/legacy/app.js"></script>
<!-- Other scripts use Vue 3 -->
<script type="module" src="/app.js"></script>
Use Cases
1. Development Without Bundlers
<!DOCTYPE html>
<html>
<head>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18",
"react-dom/client": "https://esm.sh/react-dom@18/client",
"@components/": "./src/components/",
"@utils/": "./src/utils/"
}
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from '@components/App.js';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>
2. CDN Module Loading
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// Use Three.js without npm install!
</script>
3. Path Mapping
<script type="importmap">
{
"imports": {
"@root/": "./",
"@src/": "./src/",
"@components/": "./src/components/",
"@utils/": "./src/utils/",
"@hooks/": "./src/hooks/"
}
}
</script>
<script type="module">
import Button from '@components/Button.js';
import { useAuth } from '@hooks/useAuth.js';
import { formatDate } from '@utils/dates.js';
</script>
Dynamic Import Maps
Programmatically
// Create import map dynamically
const im = document.createElement('script');
im.type = 'importmap';
im.textContent = JSON.stringify({
imports: {
'app': './app.js',
'config': `./config.${environment}.js`
}
});
document.currentScript.after(im);
Multiple Maps
// Merge multiple import maps (future spec)
// Currently only one import map supported
Browser Support
// Check support
if (HTMLScriptElement.supports && HTMLScriptElement.supports('importmap')) {
// Import maps supported
}
// Polyfill for older browsers
import 'https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js';
With Bundlers
Development vs Production
<!-- Development: Use import maps -->
<script type="importmap">
{
"imports": {
"react": "/node_modules/react/index.js"
}
}
</script>
<!-- Production: Bundled (no import map needed) -->
Build Tool Integration
// Vite supports import maps
// vite.config.js
export default {
resolve: {
alias: {
'@': '/src',
'@components': '/src/components'
}
}
}
// These aliases work with bundler in production
// And import maps in development
Best Practices
1. Use Trailing Slashes for Directories
<script type="importmap">
{
"imports": {
"components/": "./src/components/",
"components": "./src/components/index.js"
}
}
</script>
<script type="module">
import Button from 'components/Button.js'; // Directory + file
import { init } from 'components'; // Index file
</script>
2. Version Pinning
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react/": "https://esm.sh/react@18.2.0/"
}
}
</script>
3. External Dependencies
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js",
"pinia": "https://unpkg.com/pinia/dist/pinia.esm-browser.js"
}
}
</script>
4. Fallback with es-module-shims
<!-- Polyfill for Safari <16, Firefox <108 -->
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"app": "./src/app.js"
}
}
</script>
CDN Providers
esm.sh
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react-dom": "https://esm.sh/react-dom@18.2.0"
}
}
</script>
unpkg
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
jsdelivr
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4/lodash.js"
}
}
</script>
Key Takeaway
Import Maps enable bare module specifiers to work natively in browsers without bundlers. They bridge the gap between Node.js-style imports and browser ES modules. Use them for rapid prototyping, teaching, or production apps served via CDN. Combine with es-module-shims for broader browser support.