Aller au contenu principal
Site en cours de refonte — quelques pages peuvent bouger ou évoluer.
02 avril 2026

Tu bumps Rolldown et ton bundle « change » : voilà comment éviter le diff fantôme

Tu bumps Rolldown, et soudain ton bundle “change de forme”. Pas forcément un bug… mais ça peut casser du runtime. Voilà une méthode simple pour valider et déployer sans drama.

10 min de lecture
27 vues
réactions
Partager :
Tu bumps Rolldown et ton bundle « change » : voilà comment éviter le diff fantôme

Tu upgrades Rolldown, tu touches à rien côté app… et pourtant ton bundle produit un diff énorme. C’est le genre de truc qui déclenche une review parano, ou pire, une régression silencieuse en prod. Et le point important, c’est que ce n’est pas forcément « Rolldown qui déconne » : c’est parfois juste un changement de defaults côté minification.

Dans Rolldown v1.0.0-rc.7, deux réglages deviennent actifs par défaut et peuvent changer ton output sans prévenir : une minification en mode dce-only, et un inlineConst en mode « smart ». En pratique, ça veut dire moins de code, parfois plus rapide… et parfois un diff fantôme qui te fait perdre du temps. Si ton code (ou tes dépendances) est un peu borderline, ça peut même casser du runtime.

Pourquoi ton bundle change après un bump Rolldown (même si ton code n’a pas bougé)

Quand un outil de bundling change ses defaults, tu peux te retrouver avec un output différent alors que ton repo est identique. Ce n’est pas juste une question de « taille » : la minification touche à l’ordre, à la structure, au fait qu’un bout de code existe encore ou non, et à la manière dont des constantes se propagent dans le graphe de modules.

Le piège classique, c’est de traiter ça comme un diff « purement cosmétique ». En réalité, tu as deux risques : le risque opérationnel (review impossible, bruit dans l’historique, snapshot qui saute, SRI qui change), et le risque produit (un chemin de code supprimé ou modifié alors qu’il était utile dans ton runtime réel).

dce-only par défaut : ce que ça change concrètement

dce veut dire « dead code elimination ». Avec dce-only, l’idée n’est pas de faire une compression agressive façon « tout réécrire », mais d’être très bon pour supprimer ce qui est prouvé inutile : branches inaccessibles, code derrière des flags constants, exports jamais consommés, etc.

Sur une base saine, c’est plutôt une bonne nouvelle. Là où ça devient pénible, c’est quand tu as du code qui compte sur des effets de bord à l’import, sur des modules importés « pour exister » (polyfills, patchs globaux), ou sur des patterns ambigus pour un optimiseur. Dans ce cas, un « dce-only » peut te donner un output valide… mais pas équivalent à ton intention.

Le signal d’alerte que je vois souvent : un module qui était importé « innocemment » et qui, après upgrade, n’apparaît plus dans le bundle final. Pas de crash en build. Juste un comportement qui change, parfois seulement sur un parcours utilisateur précis.

inlineConst « smart » : le vrai coin où tu peux te faire surprendre

L’inlining de constantes, c’est le fait de remplacer des références par leur valeur. « Smart » suggère que l’outil essaie d’être prudent. Et la plupart du temps, ça l’est. Mais il y a un cas terrain qui mérite d’être dit franchement : toutes les « constantes » ne sont pas immuables au sens métier.

Exemple typique : une valeur exportée qui est un objet (ou une structure) et que tu considères comme une référence partagée. Techniquement, la variable est const, mais l’objet est mutable. Si l’optimiseur inline (ou duplique) ce contenu au mauvais endroit, tu peux casser une identité partagée, ou modifier l’ordre dans lequel des mutations sont observées. Dans des apps React/Next/Vite, ça arrive via des objets de config, des registres, des singletons « cheap », ou des libs qui font des caches simples.

Autre cas vicieux : les flags de build « constants » qui ne le sont pas vraiment dans la réalité, parce qu’ils sont remplacés à un autre étage (DefinePlugin-like, injection à la volée, ou build multi-variants). Si tu as un pipeline où une constante est censée varier selon l’environnement, l’inlining peut figer un comportement plus tôt que prévu.

Le « diff fantôme » : pas un bug, mais un vrai problème de delivery

Un diff de bundle, ce n’est pas juste du bruit. C’est du temps de review perdu, des artefacts CDN à invalider, des hashes qui changent partout, et parfois un faux sentiment de sécurité parce que « les tests unitaires sont verts ».

La plupart des régressions liées à la minification ne se voient pas en CI classique. Elles se voient dans le navigateur, sur un parcours, avec une dépendance qui fait un truc un peu sale, ou dans un edge case de timing. Donc mon angle, ce n’est pas « comment désactiver la minification » (même si c’est parfois le bon move), c’est « comment upgrader sans jouer à la roulette russe ».

Mon protocole de validation : comparer, mesurer, exécuter

Quand un outil change son output par défaut, je veux une validation en trois temps. D’abord, je compare le bundle en tant que fichier. Pas pour « comprendre tout le diff », mais pour voir si je suis sur un changement localisé ou sur une re-réécriture globale. Ensuite, je mesure (taille gzip/brotli, nombre de chunks, duplications suspectes). Enfin, j’exécute un minimum de runtime réel avec des smoke tests e2e.

Rolldown ajoute justement un détail utile côté diagnostic : un format Markdown pour les rapports du bundle analyzer. Dit autrement, tu peux faire sortir un rapport exploitable dans une PR, lisible par un humain, et pas juste un artefact qu’on n’ouvre jamais. Si tu veux éviter les débats stériles « le bundle a changé mais je sais pas pourquoi », c’est un bon levier.

Concrètement, je garde deux builds comparables (avant/après bump) et je fais tourner les mêmes scénarios. Sur une app React/Next, mes « scénarios » sont rarement sophistiqués : chargement d’une page clé, navigation, ouverture d’un modal lourd, interaction qui touche un store, et une page qui tape une API. Ce sont ces trucs-là qui cassent quand une optimisation a supprimé un import ou figé une constante.

# Exemple de job CI très bête mais efficace : on build, on garde les artefacts,
# et on sort des métriques de base pour comparer avant/après.

set -e

rm -rf dist
pnpm build

# Taille totale (brut) et compressée (utile pour suivre la perf)
du -sh dist | tee dist-size.txt
find dist -type f -maxdepth 3 -name "*.js" -print0 | xargs -0 gzip -k -f
find dist -type f -maxdepth 3 -name "*.js.gz" -print0 | xargs -0 du -ch | tail -n 1 | tee dist-gzip-total.txt

# Optionnel : hash global des JS pour voir si l'output a réellement changé
find dist -type f -name "*.js" -print0 | sort -z | xargs -0 shasum | shasum | tee dist-js-hash.txt

Désamorcer le risque : pin, flags de build et rollback propre

Si ton produit tourne en prod et que tu ne veux pas découvrir une régression sur un vendredi soir, le meilleur choix est souvent d’adopter progressivement. Tu pins la version de Rolldown (ou de l’outil qui l’embarque), tu introduis un flag de build pour contrôler la minification, et tu donnes à ton équipe une porte de sortie rapide.

Le point clé : ne mélange pas « upgrade Rolldown » et « activation des nouveaux defaults » dans un seul gros saut, si tu peux l’éviter. Idéalement, tu fais un bump en gardant un comportement proche de l’ancien, puis tu actives dce-only et inlineConst smart quand tu es prêt, sur une branche, avec des builds comparés et des e2e. C’est bête, mais ça transforme un incident possible en changement maîtrisé.

Côté config, l’idée est juste de rendre explicite ce qui était implicite. Les noms exacts dépendent de la manière dont tu consommes Rolldown (directement, via un wrapper, via un outil). Mais l’intention ressemble à ça : pouvoir forcer le mode « ancien comportement » le temps de valider.

// Exemple d'intention : rendre la minification explicite et contrôlable.
// Adapte aux options exactes de ta version / de ton intégration.
import { defineConfig } from "rolldown";

export default defineConfig(({ mode }) => {
  const isProd = mode === "production";

  return {
    input: "src/index.ts",
    output: {
      dir: "dist",
      format: "esm",
      sourcemap: true
    },

    // Le but : éviter qu'un bump change ton output sans que tu le voies.
    // Tu peux commencer "safe", puis activer progressivement.
    minify: isProd ? "dce-only" : false,
    inlineConst: isProd ? "smart" : "none"
  };
});

Les cas où la minification casse vraiment du runtime (et que je surveille)

Le premier, je l’ai déjà évoqué : les imports « side effects » qui disparaissent. Typiquement un polyfill, un patch global, une initialisation d’observabilité, un truc qui doit s’exécuter pour que le reste marche. Si ton bundler estime que personne n’utilise les exports, il peut considérer que le module est dispensable. Selon ton écosystème, ça se joue aussi avec les champs sideEffects dans package.json, et avec la manière dont les dépendances déclarent leurs effets de bord. Beaucoup de libs sont propres. Certaines mentent. Certaines ne savent pas.

Le second, c’est l’identité et la mutabilité. Une « constante » objet partagée entre modules, un singleton simplifié, un cache en module scope, une config mutée après import. L’inlining peut rendre les choses plus rapides, mais aussi plus surprenantes si tu avais implicitement besoin d’une seule instance. Le pire, c’est que ça peut ne casser que sur un chunk lazy-loadé ou dans un parcours où l’ordre de chargement change.

Le troisième, c’est tout ce qui touche à l’introspection ou à des comportements « non contractuels » : dépendre de Function.prototype.toString(), de noms de fonctions/classes (logs, analytics, DI maison), ou de structures d’erreurs. Ce n’est pas « recommandé », mais ça existe dans la nature, surtout quand tu embarques des libs anciennes ou des outils internes.

Mon avis (tranché) : les defaults qui changent, ça se traite comme un breaking change

Oui, l’optimisation par défaut, c’est tentant. Mais si ton outil change l’output sans que tu aies rien demandé, alors ton process doit s’adapter. Le bon réflexe, ce n’est pas de bloquer toute évolution. C’est de mettre en place un minimum de garde-fous qui te coûtent peu au quotidien : un build comparable, un rapport lisible, deux-trois smoke tests e2e qui couvrent tes parcours critiques, et une façon de rollback la minification sans rollback tout le reste.

Si tu fais ça, tu peux profiter des gains de Rolldown sans te faire piéger par le « diff fantôme ». Et tu arrêtes de découvrir des régressions à l’ancienne, dans les logs du week-end.

Sources

Cet article vous a plu ?

Commentaires

Laisser un commentaire

Entre 10 et 2000 caractères

Les commentaires sont modérés avant publication.

Aucun commentaire pour le moment.

Soyez le premier à donner votre avis !