On écrit cette ligne sans y penser dans Node, Vite ou esbuild :
import { Chart } from "chart.js";
Dans un navigateur, la même ligne bute sur un détail assez fondamental : où est chart.js ? import est compris, mais le navigateur n’a aucune raison de deviner que ce nom vient de npm, quel package.json lire, quel export choisir ou dans quel node_modules fouiller.28
Le slogan « JavaScript sans build » saute facilement cette frontière. ES modules ont standardisé le chargement des modules. Le navigateur n’est pas devenu gestionnaire de packages pour autant.
Julia Evans l’a documenté en 2024 en essayant d’utiliser plusieurs bibliothèques frontend sans chaîne de build.1 Deux ans plus tard, les import maps sont beaucoup moins exotiques : MDN les classe désormais comme largement disponibles dans les navigateurs depuis mars 2023.3 Mais les choix qu’elle a rencontrés sont toujours les mêmes. Quelqu’un doit résoudre les noms, convertir les formats incompatibles et décider si le graphe part au navigateur fichier par fichier ou sous une forme déjà regroupée.
Le navigateur veut une URL
Le navigateur est beaucoup plus heureux quand on lui donne directement une adresse :
import { draw } from "./lib/draw.js";
import confetti from "https://example.com/confetti.js";
Le premier chemin est relatif au module courant. Le second est déjà une URL. Il n’y a presque rien à deviner.2
import "chart.js" est différent. C’est un bare specifier, un nom sans ./, ../, / ni schéma d’URL. Dans Node, l’environnement possède des règles de résolution de packages et lit notamment les champs main, exports, type et les exports conditionnels du package.json.8 Dans un document web, ce nom doit être relié à une URL par une import map.23
<script type="importmap">
{
"imports": {
"chart.js": "/vendor/chart.js"
}
}
</script>
Avec cette table, chart.js cesse d’être un surnom mystérieux. Les scopes peuvent même changer la résolution selon la branche du graphe, par exemple pour faire cohabiter plusieurs versions.3
Mais la carte doit exister avant les modules qui en dépendent. Et sa valeur finale reste une URL. La magie npm n’a pas migré dans le mot-clé import ; on lui a donné une table explicite.
2024 a vieilli
Dans son billet de novembre 2024, Evans jugeait encore les import maps assez nouvelles pour réserver leur usage à des projets expérimentaux ou peu exposés.1 Cette prudence était raisonnable au moment du billet. Elle ne décrit plus très bien le support navigateur de 2026.
MDN marque aujourd’hui type="importmap" comme Baseline Widely available, avec disponibilité générale depuis mars 2023.3 modulepreload, autre pièce utile pour des graphes ESM natifs, est indiqué comme largement disponible depuis septembre 2023.4
En 2026, le vieux doute sur Safari est devenu secondaire. La question pénible arrive ensuite : qu’est-ce que le package a réellement publié pour le web ?
Un package npm, après tout, n’est pas un format de navigateur.
Trois paquets, trois vies
Evans revient alors à une méthode de détective assez efficace : ne pas faire confiance à .js, ouvrir ce qui a été publié et identifier trois familles — classique/UMD, ESM, CommonJS.1
Un fichier UMD peut déjà contenir le travail de packaging nécessaire au navigateur. Pour Chart.js, Evans choisit ainsi chart.umd.js, l’ajoute avec un simple <script src> et copie le fichier dans son dépôt.1 Son projet n’a alors aucun build local. Le fichier, lui, est bien un artefact de build publié par l’auteur de la bibliothèque.
Un package ESM peut être servi directement si ses imports forment un graphe que le navigateur sait résoudre. Si tous les imports sont relatifs ou absolus, c’est presque immédiat. Si le package importe à son tour nanoid, @atcute/client ou d’autres bare specifiers, il faut une import map couvrant ces noms.13
Un package CommonJS utilise au contraire des mécanismes comme require() ou module.exports. Ce n’est pas la syntaxe native des modules web. Evans tombe sur ce cas avec @atproto/oauth-client-browser et passe par esm.sh pour obtenir une entrée ESM utilisable dans le navigateur.1
La documentation d’esm.sh casse elle-même l’illusion : par défaut, le service transforme le code et le bundle si nécessaire.5
Le build est toujours là. Il tourne juste sur la machine de quelqu’un d’autre.
La carte devient manifeste
Deux entrées dans une import map se gèrent à la main. Vingt sous-chemins et quelques dépendances transitives, beaucoup moins élégamment.
C’est précisément le travail que JSPM Generator automatise aujourd’hui. Son API prend des specifiers, trace leurs dépendances, choisit des résolutions adaptées aux environnements browser et module, puis produit la carte.6 Son générateur web peut sortir directement un document HTML avec import map, intégrité et preloads.7
Même le workflow le plus « natif » finit donc par réintroduire un outil de résolution. Il ne fusionne pas nécessairement votre JavaScript en un gros fichier ; il calcule tout de même le graphe qui permet au navigateur de savoir où aller.
Cette séparation est plutôt saine. Un bundler mélange souvent plusieurs tâches dans une commande. L’import map permet d’en retirer une — la résolution — et de la rendre visible sous forme de données.
Le prix est que cette donnée devient une pièce de votre déploiement. Les versions, URLs, scopes et éventuellement hashes d’intégrité doivent suivre vos mises à jour.
Le graphe coûte des requêtes
Le navigateur ne se plaint pas parce qu’un graphe contient cinquante modules. Le réseau, lui, voit toujours cinquante ressources potentielles.
Avec un module d’entrée classique, le navigateur charge le fichier, l’analyse, découvre ses imports, puis demande les dépendances. Il découvre ensuite les imports de ces dépendances. MDN explique que rel="modulepreload" existe pour anticiper ce processus : les modules peuvent être téléchargés, analysés et compilés plus tôt plutôt que d’attendre leur découverte séquentielle.4
Evans a rencontré la version très concrète du problème : son import map faisait charger des dizaines de fichiers et son serveur de développement local échouait parfois à tous les servir correctement.1 Elle précise que le problème disparaissait en production, donc ce n’est pas une mesure universelle contre l’ESM natif. C’est tout de même un rappel utile : enlever le bundle rend le graphe réseau visible.
modulepreload peut réduire cette latence de découverte. Il ne transforme pas quarante fichiers en un.
À l’inverse, esm.sh explique que son bundling de sous-modules sert justement à réduire le nombre de requêtes réseau ; sa documentation avertit aussi qu’un regroupement trop agressif peut dupliquer des modules partagés ou changer certains comportements autour de import.meta.url et des side effects.5
Ce n’est pas un duel entre performance et pureté. La vraie décision est plus terre-à-terre : chunks générés, ou graphe HTTP explicite ?
HTTP entre dans l’atelier
En retirant le serveur de dev sophistiqué, on redécouvre aussi des règles très web et très peu glamour.
MDN rappelle que les modules chargés depuis une autre origine passent par CORS et doivent être servis avec un type MIME JavaScript valide.2 Ouvrir simplement index.html en file:// peut provoquer des erreurs de sécurité ; il faut généralement un serveur HTTP local.2
Un bundler masque souvent ce genre de détails pendant le développement parce qu’il lance justement un serveur, réécrit les chemins et produit des fichiers qu’il sait servir. Retirer l’outil remet ces responsabilités au premier plan.
Pour une petite page, ce retour des contraintes web peut même être agréable : trois fichiers lisibles, un serveur statique, dix lignes d’import map, terminé. Le gain vient de la réduction de votre mécanique, pas de la disparition des règles de la plateforme.
Qu’est-ce qu’on retire vraiment ?
Le terme devient beaucoup plus utile dès qu’on précise ce qui a réellement été supprimé.
UMD copié localement : aucun build dans votre projet, presque aucun graphe à gérer, mais vous consommez le build déjà fabriqué par l’auteur.
ESM natif + import map : aucune transformation ni bundle obligatoire ; vous gardez les fichiers séparés et rendez la résolution explicite. Un générateur comme JSPM peut encore préparer la carte.67
Import depuis esm.sh : aucune chaîne de build locale, mais un service distant effectue la conversion et parfois le bundling.5
CommonJS local brut : ce n’est pas un workflow navigateur. Il faut transformer le package ou choisir une autre distribution.1
Le navigateur de 2026 est nettement plus capable qu’il y a dix ans : import, import maps, modulepreload, tout cela est natif. Il ne connaît toujours pas votre installation npm, ne sélectionne pas tout seul l’export conditionnel qui vous arrange et n’avale pas CommonJS, TypeScript ou JSX par pure bonne volonté.
Le bundler peut disparaître de votre terminal.
Ses responsabilités, elles, doivent toujours avoir une adresse.