Économiser 26,8 Ko en précompilant les messages i18n

Performancei18n· 3 min de lecture

Ajouter next-intl (voir l'article précédent) a résolu le routage multilingue. Cela a aussi ajouté une dépendance à l'analyseur de messages ICU de @formatjs, qui par défaut s'exécute dans le navigateur — le client de chaque visiteur télécharge et exécute la mécanique qui analyse une syntaxe de message de type {minutes, plural, one {...} other {...}}, alors même que chaque message réel de ce site est connu au moment du build et ne change jamais selon la requête.

Mesurer avant de supposer

Plutôt que d'estimer le coût, j'ai utilisé l'analyseur de bundle natif Turbopack intégré à Next.js 16.1+ (npx next experimental-analyze) pour obtenir une vraie carte des modules. Il a révélé un bloc dédié @formatjs/ de 22,88 Ko en tête du bundle client — pas une erreur d'arrondi, et pas quelque chose que les audits Lighthouse bootup-time/mainthread-work-breakdown (tous deux signalés à un score de 0 sur chaque test de ce site jusqu'ici) allaient expliquer seuls sans ouvrir directement la carte des modules.

Note

J'avais d'abord utilisé @next/bundle-analyzer par habitude, puisque c'est l'outil le plus souvent référencé. Le build a immédiatement signalé qu'il n'est pas compatible avec les builds Turbopack — une vraie limitation documentée, pas une erreur de configuration de ma part. Je l'ai retiré et suis passé à l'analyseur natif de Next plutôt que de contourner l'incompatibilité.

Le correctif

next-intl propose une option experimental.messages.precompile qui déplace l'analyse ICU vers le moment du build plutôt que le client. Les dictionnaires de messages sont compilés dans un format que le runtime peut consommer directement, sans aucun analyseur nécessaire dans le navigateur.

const withNextIntl = createNextIntlPlugin({
  requestConfig: "./i18n/request.ts",
  experimental: {
    messages: {
      path: "./messages",
      format: "json",
      locales: "infer",
      precompile: true,
    },
  },
});

Activer cette option a immédiatement provoqué une vraie erreur TypeScript — le type de la version installée pour experimental.messages exige un champ locales que l'exemple minimal de la documentation officielle de cette option précise omet. Ajouter locales: "infer" (détection automatique depuis le dossier messages/) a corrigé le problème.

Vérifier le gain réel

Decision

Je n'ai pas pris l'existence de la fonctionnalité comme preuve qu'elle fonctionnait — j'ai relancé l'analyseur de bundle avant et après, puis comparé les chiffres réels, avec la même rigueur que pour l'investigation Lighthouse de l'article ci-dessus.

Le total « All Route Modules » est passé de 386,56 Ko à 359,76 Ko compressés — une réduction de 26,8 Ko, correspondant à peu près au bloc @formatjs/ identifié plus tôt (désormais entièrement absent de la carte des modules, à l'exception d'un résidu marginal). Le nombre de modules est passé de 227 à 224. npm run build et npm test sont restés propres ensuite — 24/24 pages, suite de tests complète passante.

Ce que je ferais différemment

Rien dans la séquence — mesurer, formuler une hypothèse sur la cause, appliquer un correctif ciblé, remesurer pour confirmer le gain réel plutôt que de faire confiance à la description de la fonctionnalité. La seule chose que je changerais à l'avenir : vérifier toute la surface de type d'une option de configuration par rapport à la version installée avant d'écrire l'exemple minimal de la documentation, puisque c'est la deuxième fois le même jour que cet écart m'a piégé (voir l'incident hasLocale de l'article précédent).