On choisit souvent son gestionnaire de paquets sur un chiffre : "18× plus rapide", "25× plus rapide", "50 à 70 % de disque en moins". Ces chiffres viennent presque toujours du vendeur de l'outil, mesurés sur un cache froid, dans un projet React + TypeScript neuf. Autant dire l'inverse exact d'une CI d'équipe, où le cache est chaud, où l'installation dure quelques secondes et où le vrai sujet est ailleurs.
Ce qui coûte cher en intégration continue, ce n'est pas la durée de install. C'est le jour où une dépendance exécute son postinstall avec le jeton du dépôt, où une pull request de fork écrit dans le cache restauré par la branche principale, ou où un package.json modifié passe la CI sans que le lockfile suive. Sur ces quatre points - le verrouillage du lockfile, le droit d'exécution des scripts, la confiance accordée au cache et la gouvernance du projet - les quatre gestionnaires ne se ressemblent pas du tout.
Le lockfile : qui refuse de travailler sans ?
C'est la différence la plus concrète et la plus facile à vérifier dans les documents officiels.
pnpm bascule automatiquement en mode frozen-lockfile dès qu'il détecte la CI, et depuis la version 11 il échoue franchement si le lockfile a été écrit par une majeure plus récente, au lieu de le réécrire en silence. Conséquence pratique : la version de pnpm épinglée dans la CI doit correspondre à celle qui a généré le lockfile, sinon la construction casse.
Yarn active --immutable par défaut en CI : si le lockfile devait être modifié, la commande sort en erreur. Le pendant pour le dossier de cache existe aussi (--immutable-cache).
Bun fait exception : sa documentation indique explicitement qu'il n'active pas --frozen-lockfile tout seul en CI. Il faut passer le drapeau ou utiliser bun ci. En cas de désaccord entre package.json et bun.lock, l'erreur est nette et nomme la première dépendance modifiée. Détail intéressant : certaines retouches qui laissent toutes les versions verrouillées inchangées - passer de ^1.0.0 à >=1.0.0, déplacer une dépendance d'un groupe à l'autre - ne font pas échouer l'installation. Le contrôle porte sur les versions résolues, pas sur le texte du fichier.
npm demande de choisir la bonne commande : npm ci et non npm install. La documentation de npm ci signale un piège fréquent : si le package-lock.json a été produit avec des options qui changent la forme de l'arbre, comme --legacy-peer-deps ou --install-links, il faut repasser les mêmes options à npm ci, sinon l'installation échoue. Le remède recommandé est de figer ces options dans un .npmrc versionné.

| Gestionnaire | Comportement en CI | Piège principal |
|---|---|---|
| npm | npm ci obligatoire, sinon rien n'est verrouillé |
options de lockfile à répéter à l'identique |
| pnpm | frozen-lockfile automatique, échec si lockfile d'une majeure ultérieure |
version de pnpm à aligner sur le lockfile |
| Yarn | --immutable par défaut |
cache versionné à contrôler séparément |
| Bun | rien par défaut : --frozen-lockfile ou bun ci |
un drapeau oublié rend l'installation silencieusement permissive |
Les scripts d'installation : qui exécute le code des dépendances ?
Une dépendance npm peut lancer du code arbitraire au moment de l'installation. Pendant longtemps, les quatre outils le faisaient sans discuter. Trois ont changé de politique.
pnpm, depuis la version 10, n'exécute plus les scripts de cycle de vie des dépendances sauf si elles figurent dans onlyBuiltDependencies. Refus par défaut, liste blanche explicite.
Bun ne les exécute pas non plus, sauf mention dans trustedDependencies. Il optimise même les scripts des paquets courants comme esbuild ou sharp en déterminant lesquels ont réellement besoin de tourner, et exécute les autres en parallèle.
npm a ajouté un champ allowScripts dans package.json, avec des refus explicites possibles, et un --strict-allow-scripts qui transforme tout script non examiné en erreur dure plutôt qu'en simple avertissement. Passer --allow-scripts en ligne de commande sur un npm install ou npm ci de projet est d'ailleurs une erreur : la politique doit vivre dans le dépôt, pas dans le YAML de la CI. Le contournement --dangerously-allow-all-scripts existe et porte bien son nom.
Yarn conserve un interrupteur global (enableScripts) plutôt qu'une liste blanche par dépendance, avec --mode=skip-build pour couper l'étape de construction sans modifier les artefacts générés.
Ce refus par défaut est une vraie amélioration, mais ce n'est pas une frontière de sécurité. La preuve : CVE-2025-69264. Dans pnpm v10, le blocage des scripts passe par une liste blanche consultée à la phase de build. Or les dépendances hébergées sur git sont préparées à la phase de fetch, avec une option distincte (ignoreScripts, à false par défaut) qui ne consulte jamais cette liste. Un prepare malveillant s'exécutait donc pendant pnpm install, sans avertissement ni approbation. La faille a été corrigée dans la branche v10 ; selon les analyses publiées, les versions antérieures à 10.26.0 restent exposées.
La leçon vaut pour les quatre : un refus par défaut vaut ce que vaut sa couverture. La CI doit garder ses propres garde-fous - jetons en lecture seule par défaut, pas de secrets exposés aux jobs de fork, contrôle des sorties réseau.
Le cache est un objet de confiance, pas un accélérateur neutre
C'est le point le plus sous-estimé, et les documents officiels le disent sans détour.
La documentation CI de pnpm qualifie le store et le cache de métadonnées de "trusted caches" : il ne faut jamais laisser un job non fiable écrire dans un store qu'un job de confiance restaurera ensuite. Elle précise aussi, avec une honnêteté rare chez un éditeur, que mettre le store en cache n'est pas obligatoire et ne garantit pas une installation plus rapide.
Yarn va plus loin parce que son mode Zero-Installs suppose de versionner le cache dans le dépôt. Sa documentation recommande donc --check-cache en CI lorsqu'on accepte des pull requests de tiers : la commande retélécharge les paquets et vérifie les sommes de contrôle, faute de quoi un contributeur pourrait altérer les paquets versionnés avant de soumettre sa PR. C'est l'aveu le plus clair qu'une optimisation de performance déplace une frontière de confiance.
Bun documente la combinaison --offline --frozen-lockfile, qui rend une installation CI entièrement déterministe et sans réseau à condition que le cache soit complet. Attention à la nuance : seuls les paquets effectivement placés dans node_modules sont mis en cache, et une installation --production ou --omit ne peuple pas les groupes qu'elle exclut.
La gouvernance : aucun choix n'est neutre
En décembre 2025, Anthropic a annoncé le rachat de Bun, présenté comme l'infrastructure de Claude Code - un produit dont la société annonçait le milliard de dollars de revenus annualisés six mois après l'ouverture au public. Anthropic s'est engagée à maintenir Bun open source sous licence MIT et à continuer d'investir dans le runtime, le gestionnaire de paquets, le bundler et le lanceur de tests. Les chiffres de popularité cités dans l'annonce - plus de 7 millions de téléchargements mensuels, plus de 82 000 étoiles GitHub - viennent d'Anthropic elle-même.
Ce n'est pas un argument contre Bun, c'est une donnée de due diligence. La licence MIT laisse la sortie ouverte ; elle ne dit rien de l'ordre des priorités futures, qui sera celui d'un agent de codage commercial. Symétriquement, npm vit dans l'écosystème GitHub/Microsoft, pnpm et Yarn sont portés par de petites équipes de mainteneurs. La bonne question n'est pas "lequel est neutre" - aucun ne l'est - mais "quelle dépendance de gouvernance mon équipe peut absorber".
Un signe à surveiller au passage : la documentation CI de pnpm a abandonné Corepack, au motif que celui-ci installe un shim JavaScript, si bien que chaque appel à pnpm paie d'abord le démarrage de Node. Dans un job qui appelle pnpm des dizaines de fois, ce coût n'est pas nul. C'est aussi un rappel que la couche d'installation d'une CI est elle-même un composant à versionner.
La facture réelle n'est pas dans les minutes de CI
Reprenons les chiffres des comparatifs publiés. Le "18× plus rapide que npm" vient des benchmarks Bun v1.0 sur un projet React + TypeScript avec cache propre. Le "25×" est la promesse de la documentation de Bun. Le "2 à 3× plus rapide, 50 à 70 % de disque en moins" vient des benchmarks de pnpm et de reproductions communautaires. Un comparatif d'éditeur résume les positions reçues : pnpm gagnant sur la vitesse et le disque, Bun plus rapide encore mais à traiter comme un changement de runtime plutôt qu'un remplacement à coût nul, Yarn v4 comme valeur sûre par défaut, npm ayant comblé l'essentiel de l'écart sur les installations classiques sans store adressable par contenu.
Le même comparatif fait l'arithmétique de la CI : dix secondes gagnées par déploiement, cinquante déploiements par jour, soit un peu plus de huit minutes récupérées quotidiennement. Huit minutes. En face, il y a une semaine de migration pour une équipe, des dépendances qui cassaient déjà mais ne se plaignaient pas, et un parc d'outils qui suppose un node_modules aplati.
Car c'est là que se trouve le véritable coût, et il est rarement chiffré dans les comparatifs. npm expose un --install-strategy=linked, qui installe dans node_modules/.store avec des liens et sans remontée de dépendances : la documentation npm le recommande aux auteurs de paquets pour faire échouer les dépendances fantômes avant publication. pnpm applique cette isolation par défaut, et Bun propose désormais les deux dispositions, aplatie ou isolée avec store sous node_modules/.bun. Le bénéfice est réel : une dépendance importée sans être déclarée casse en CI au lieu de casser en production. Le coût l'est aussi : le code qui "marchait" révèle une dette, et il faut la traiter avant de pouvoir livrer.
Ce qu'il faut vérifier avant de trancher
Quel que soit l'outil retenu, quatre règles font l'essentiel du travail, et elles ne dépendent pas du benchmark :
- Épingler la version du gestionnaire dans
package.jsonet l'utiliser aussi en CI, pour que le lockfile et l'outil qui le lit soient la même majeure. - Choisir en CI la commande qui échoue :
npm ci,pnpm installavec le mode gelé,yarn installavec--immutable,bun ciou--frozen-lockfile. La valeur par défaut n'est pas un contrat. - Nommer les dépendances autorisées à exécuter des scripts, dans le dépôt, et activer le mode strict là où il existe.
- Séparer les caches par niveau de confiance : aucun job déclenché par un fork n'écrit dans un store ou un cache restauré par la branche principale.
Le gestionnaire le plus rapide n'est pas nécessairement le meilleur choix. Le bon est celui dont les refus correspondent à ce que votre équipe est prête à assumer, et dont les décisions restent lisibles dans un diff. Le reste, ce sont des secondes.