Pourquoi parler des import maps aujourd’hui ?
Dans la plupart des projets frontend modernes, les dépendances JavaScript passent par un bundler : Vite, Webpack, Rollup, Parcel, Turbopack ou l’outillage intégré à un framework. C’est souvent pertinent : optimisation, transformation TypeScript, découpage du code, minification, intégration CSS, hot reload, compatibilité navigateur.
Mais cette habitude a parfois un effet secondaire : on oublie que le navigateur sait déjà charger des modules JavaScript natifs avec import et export. Les import maps ajoutent une brique importante à ce modèle : elles permettent de dire au navigateur comment résoudre certains chemins d’import.
Autrement dit, au lieu d’écrire uniquement ceci :
import { createApp } from './vendor/vue.esm-browser.js';
on peut écrire ceci :
import { createApp } from 'vue';
et laisser une carte de résolution déclarée dans le HTML expliquer que vue correspond à une URL précise.
Ce sujet complète bien les réflexions sur le progressive enhancement, les Custom Elements et les architectures frontend qui ne reposent pas systématiquement sur une compilation lourde.
Le problème résolu par les import maps
Les modules ES natifs fonctionnent très bien avec des chemins relatifs ou absolus :
import { formatDate } from './utils/date.js';
import { UserCard } from '/components/user-card.js';
En revanche, un import comme celui-ci ne suffit pas dans un navigateur :
import { z } from 'zod';
Dans Node.js ou dans un projet bundlé, zod est résolu via node_modules. Le navigateur, lui, ne sait pas ce que signifie ce nom nu, aussi appelé bare specifier. Il attend une URL.
Une import map sert précisément à transformer ces noms logiques en chemins concrets.
<script type="importmap">
{
"imports": {
"zod": "https://esm.sh/zod@3.25.0",
"@app/": "/src/"
}
}
</script>
<script type="module" src="/src/main.js"></script>
Dans /src/main.js, on peut maintenant écrire :
import { z } from 'zod';
import { createUserSchema } from '@app/schemas/user.js';
const userSchema = createUserSchema(z);
La carte dit deux choses au navigateur :
zodcorrespond à une dépendance distante ;- tout import qui commence par
@app/doit pointer vers/src/.
Le préfixe avec slash final est important. Dans une import map, @app/ et @app ne signifient pas la même chose : le premier sert à résoudre une famille de chemins, le second un identifiant exact.
Exemple minimal sans bundler
Prenons une petite application HTML, CSS et JavaScript qui utilise une dépendance externe et quelques modules internes.
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Démo import maps</title>
<script type="importmap">
{
"imports": {
"nanoid": "https://esm.sh/nanoid@5.1.5",
"@app/": "/src/"
}
}
</script>
</head>
<body>
<main>
<h1>Utilisateurs</h1>
<button id="add-user">Ajouter</button>
<ul id="users"></ul>
</main>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Le fichier principal reste lisible :
import { nanoid } from 'nanoid';
import { renderUser } from '@app/render-user.js';
const button = document.querySelector('#add-user');
const list = document.querySelector('#users');
button?.addEventListener('click', () => {
const user = {
id: nanoid(),
name: `Utilisateur ${list.children.length + 1}`
};
list.append(renderUser(user));
});
Et le module de rendu est indépendant :
export function renderUser(user) {
const item = document.createElement('li');
item.textContent = `${user.name} — ${user.id}`;
return item;
}
Ce n’est pas un remplacement universel de Vite ou Next.js. Mais pour un prototype, une documentation interactive, un support de formation, un composant isolé ou une page simple, cette approche peut être très agréable.
Organiser les alias internes
Les import maps ne servent pas seulement aux dépendances externes. Elles peuvent aussi rendre les imports internes plus stables.
Sans alias, une arborescence profonde finit vite par produire des chemins fragiles :
import { formatPrice } from '../../../shared/formatters/price.js';
import { createButton } from '../../ui/button.js';
Avec une import map :
<script type="importmap">
{
"imports": {
"@shared/": "/src/shared/",
"@ui/": "/src/ui/",
"@features/": "/src/features/"
}
}
</script>
Les imports deviennent plus expressifs :
import { formatPrice } from '@shared/formatters/price.js';
import { createButton } from '@ui/button.js';
Cette convention ressemble aux alias configurés dans TypeScript, Vite ou Webpack, mais elle est comprise directement par le navigateur. Dans un projet TypeScript, il faudra toutefois garder les configurations synchronisées.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@shared/*": ["src/shared/*"],
"@ui/*": ["src/ui/*"],
"@features/*": ["src/features/*"]
}
}
}
C’est un point d’architecture à ne pas négliger : si le navigateur, TypeScript, les tests et l’éditeur ne résolvent pas les chemins de la même manière, l’équipe perdra vite confiance dans le système.
Gérer plusieurs versions avec scopes
Les import maps permettent aussi de définir des règles différentes selon le module importeur grâce à scopes. C’est plus avancé, mais très utile pour comprendre le modèle.
<script type="importmap">
{
"imports": {
"date-lib": "/vendor/date-lib-v2.js"
},
"scopes": {
"/legacy/": {
"date-lib": "/vendor/date-lib-v1.js"
}
}
}
</script>
Ici, les modules standards qui importent date-lib recevront la version 2. Les modules situés sous /legacy/ recevront la version 1.
Ce mécanisme peut faciliter une migration progressive. On peut moderniser une partie de l’application sans imposer immédiatement la même version d’une dépendance à tout le code. Il faut cependant rester prudent : multiplier les versions augmente le poids réseau, complique le débogage et peut créer des incohérences d’état.
Import maps et performance
Une import map ne bundle pas le code. Si votre application importe cinquante petits modules, le navigateur devra potentiellement charger cinquante fichiers. Ce n’est pas toujours problématique avec HTTP/2 ou HTTP/3, mais cela doit être mesuré.
Pour une petite interface, la simplicité peut compenser largement. Pour une application dense, un bundler reste souvent préférable, notamment pour :
- regrouper certains modules ;
- supprimer le code mort ;
- minifier les fichiers ;
- transformer TypeScript, JSX ou Vue ;
- produire des chunks optimisés ;
- gérer les assets CSS, images et polices.
Les import maps s’inscrivent donc dans une logique de choix technique, pas dans une opposition dogmatique. Comme pour le cache frontend ou l’optimisation des images web, la bonne réponse dépend du contexte, de la taille du projet et des contraintes de livraison.
Pour améliorer le chargement, on peut aussi précharger certains modules critiques :
<link rel="modulepreload" href="/src/main.js">
<link rel="modulepreload" href="/src/ui/button.js">
Le préchargement doit rester ciblé. Précharger tout le graphe de dépendances revient souvent à annuler les bénéfices d’un chargement progressif.
Sécurité et dépendances distantes
Charger une dépendance depuis un CDN comme esm.sh, jspm.io ou skypack.dev est pratique pour expérimenter. En production, la question mérite plus de rigueur.
Une URL distante peut devenir indisponible, changer de comportement selon les paramètres, introduire une latence supplémentaire ou dépendre d’un service tiers. Il est souvent préférable de figer précisément les versions, voire d’héberger soi-même les fichiers critiques.
<script type="importmap">
{
"imports": {
"lit": "/vendor/lit/lit-3.3.0.js"
}
}
</script>
Cette approche rend le déploiement plus prévisible. Elle facilite aussi les audits, la revue de code et la reproductibilité des environnements.
La documentation MDN sur les import maps et la spécification du WICG sont de bonnes références pour approfondir les détails de résolution.
Où les utiliser concrètement ?
Les import maps sont particulièrement adaptées à plusieurs situations :
- supports pédagogiques où l’on veut montrer le JavaScript natif sans configuration ;
- prototypes rapides servis par un simple serveur statique ;
- micro-frontends ou îlots autonomes ;
- documentations interactives ;
- pages d’administration internes simples ;
- migrations progressives depuis un ancien code non bundlé.
Elles sont moins adaptées aux grosses applications qui utilisent massivement TypeScript, JSX, CSS Modules, PostCSS, transformations d’assets, tests intégrés et découpage avancé du code. Dans ces cas, un outillage de build reste plus confortable.
On peut aussi combiner les deux mondes : utiliser un bundler pour l’application principale, mais conserver des import maps pour certains modules externes, des démonstrations ou des intégrations spécifiques.
Bonnes pratiques à retenir
Une import map doit rester courte, explicite et versionnée avec le code. Si elle devient un fourre-tout de dizaines d’alias ambigus, elle reproduit les mêmes problèmes qu’une configuration de bundler mal entretenue.
Privilégiez des alias stables :
{
"imports": {
"@app/": "/src/",
"@ui/": "/src/ui/",
"@domain/": "/src/domain/"
}
}
Évitez les alias trop vagues comme utils/, components/ ou lib/ si leur signification varie selon les équipes. Un bon alias décrit une frontière d’architecture, pas seulement un dossier pratique.
Enfin, testez toujours dans un vrai navigateur. Les import maps concernent la résolution au runtime : une configuration qui semble correcte dans l’éditeur peut échouer si le chemin généré ne correspond pas à un fichier servi par le serveur.
Conclusion
Les import maps rappellent une idée importante : le navigateur moderne est une plateforme de plus en plus capable. Il sait charger des modules, résoudre des graphes de dépendances, précharger des ressources et exécuter des applications structurées sans forcément passer par une compilation systématique.
Cela ne rend pas les bundlers obsolètes. Cela ajoute une option. Pour enseigner, prototyper, isoler un composant ou simplifier une petite application, les import maps offrent un excellent compromis entre lisibilité, contrôle et faible complexité d’outillage.
Leur intérêt principal n’est pas de supprimer tous les outils, mais de mieux comprendre ce que ces outils font pour nous. Et cette compréhension rend les choix d’architecture frontend beaucoup plus solides.