Posez votre question et obtenez un résumé du document en referencant cette page et le Provider AI de votre choix
Historique des versions
- "Ajout du comparatif des étoiles GitHub"v8.9.818/05/2026
- "Initialisation du benchmark"v8.7.506/01/2026
Le contenu de cette page a été traduit à l'aide d'une IA.
Voir la dernière version du contenu original en anglaisIf you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
Bibliothèques i18n Next.js - Rapport de Benchmark 2026
Cette page est un rapport de benchmark pour les solutions i18n sur Next.js.
Table des Matières
Benchmark Interactif
Référence des résultats :
Voir les données complètes du benchmark
Voir le dépôt complet du benchmark ici.
Introduction
Les bibliothèques d'internationalisation ont un impact lourd sur votre application. Le risque principal est de charger du contenu pour chaque page et chaque langue alors que l'utilisateur ne visite qu'une seule page.
À mesure que votre application grandit, la taille du bundle peut augmenter de manière exponentielle, ce qui peut nuire considérablement aux performances.
Par exemple, pour les cas les plus critiques, une fois internationalisée, votre page peut finir par être près de 4 fois plus volumineuse.
Un autre impact des bibliothèques i18n est le ralentissement du développement. Transformer des composants en contenu multilingue à travers plusieurs langues prend du temps.
Parce que le problème est complexe, de nombreuses solutions existent - certaines axées sur la DX (expérience développeur), d'autres sur la performance ou la scalabilité, etc.
Intlayer tente d'optimiser l'ensemble de ces dimensions.
TL;DR
- Intlayer & next-translate : Meilleurs choix pour la performance Next.js, offrant l'empreinte la plus faible et le meilleur support du rendu statique.
- next-intl : L'option la plus tendance mais lourde et complexe à optimiser pour les grandes applications.
- next-i18next : Populaire et riche en plugins, mais porte un poids de bundle significatif (~3× Intlayer).
- À éviter : gt-next et lingo.dev en raison de graves problèmes de performance, de verrouillage propriétaire et de bugs cassant le build.
Testez votre application
Pour mettre en lumière ces problèmes, j'ai construit un scanner gratuit que vous pouvez essayer ici.
Le problème
Il existe deux manières majeures de limiter l'impact d'une application multilingue sur votre bundle :
- Diviser votre JSON (ou contenu) par fichiers / variables / namespaces afin que le bundler puisse "tree-shaker" le contenu inutilisé pour une page donnée
- Charger dynamiquement le contenu de votre page uniquement dans la langue de l'utilisateur
Limitations techniques pour ces approches :
Chargement dynamique
Même lorsque vous déclarez des routes comme [locale]/page.tsx, avec Webpack ou Turbopack, et même si generateStaticParams est défini, le bundler ne traite pas locale comme une constante statique. Cela signifie qu'il peut inclure le contenu de toutes les langues dans chaque page. Le principal moyen de limiter cela est de charger le contenu via un import dynamique (ex: import('./locales/${locale}.json')).
Ce qui se passe au moment du build, c'est que Next.js émet un bundle JS par locale (ex: ./locales_fr_12345.js). Une fois le site envoyé au client, lors de l'exécution de la page, le navigateur effectue une requête HTTP supplémentaire pour le fichier JS nécessaire (ex: ./locales_fr_12345.js).
Une autre façon de traiter le même problème est d'utiliserfetch()pour charger le JSON dynamiquement. C'est ainsi queTolgeefonctionne lorsque le JSON se trouve sous/public, ounext-translate, qui s'appuie surgetStaticPropspour charger le contenu. Le flux est le même : le navigateur fait une requête HTTP supplémentaire pour charger l'asset.
Découpage du contenu (Splitting)
Si vous utilisez une syntaxe comme const t = useTranslation() + t('mon-objet.mon-sous-objet.ma-cle'), l'intégralité du JSON doit généralement être présent dans le bundle pour que la bibliothèque puisse le parser et résoudre la clé. Une grande partie de ce contenu est donc expédiée même lorsqu'il est inutilisé sur la page.
Pour atténuer cela, certaines bibliothèques vous demandent de déclarer par page quels namespaces charger - ex: next-i18next, next-intl, lingui, next-translate, next-international.
En revanche, Paraglide ajoute une étape supplémentaire avant le build pour transformer le JSON en symboles plats comme const en_my_var = () => 'ma valeur'. En théorie, cela permet au tree-shaking de supprimer le contenu inutilisé sur la page. Comme nous le verrons, cette méthode présente tout de même des compromis.
Enfin, Intlayer applique une optimisation au moment du build afin que useIntlayer('ma-cle') soit remplacé directement par le contenu correspondant.
Méthodologie
Pour ce benchmark, nous avons comparé les bibliothèques suivantes :
Base App(Pas de bibliothèque i18n)next-intlayer(v8.7.12)next-i18next(v16.0.5)next-intl(v4.9.1)@lingui/core(v5.3.0)next-translate(v3.1.2)next-international(v1.3.1)@inlang/paraglide-js(v2.15.1)@tolgee/react(v7.0.0)@lingo.dev/compiler(v0.4.0)wuchale(v0.22.11)gt-next(v6.16.5)
J'ai utilisé Next.js en version 16.2.4 avec l'App Router.
J'ai construit une application multilingue avec 10 pages et 10 langues.
J'ai comparé quatre stratégies de chargement :
Ouvrir le tableau dans une fenêtre modale pour voir tout le contenu clairement
| Stratégie | Sans namespaces (global) | Avec namespaces (scoped) |
|---|---|---|
| Chargement statique | Static : Tout en mémoire au démarrage. | Scoped static : Divisé par namespace ; tout chargé au démarrage. |
| Chargement dynamique | Dynamic : Chargement à la demande par locale. | Scoped dynamic : Chargement granulaire par namespace et locale. |
Résumé des stratégies
- Static : Simple ; pas de latence réseau après le chargement initial. Inconvénient : taille de bundle importante.
- Dynamic : Réduit le poids initial (lazy-loading). Idéal lorsque vous avez de nombreuses locales.
- Scoped static : Organise bien le code (séparation logique) sans requêtes réseau supplémentaires complexes.
- Scoped dynamic : Meilleure approche pour le code splitting et la performance. Minimise la mémoire en ne chargeant que ce dont la vue actuelle et la locale active ont besoin.
Ce que j'ai mesuré :
J'ai fait tourner la même application multilingue dans un navigateur réel pour chaque stack, puis j'ai noté ce qui passait réellement sur le réseau et combien de temps les choses prenaient. Les tailles sont reportées après compression web normale, car c'est ce qui est le plus proche de ce que les gens téléchargent réellement.
Taille de la bibliothèque d'internationalisation : Après bundling, tree-shaking et minification, la taille de la bibliothèque i18n est la taille des providers (ex:
NextIntlClientProvider) + le code des hooks (ex:useTranslations) dans un composant vide. Cela n'inclut pas le chargement des fichiers de traduction. Cela répond à la question du coût de la bibliothèque avant même que votre contenu n'entre en jeu.JavaScript par page : Pour chaque route du benchmark, quelle quantité de script le navigateur récupère pour cette visite, moyennée sur l'ensemble des pages de la suite (et sur l'ensemble des locales lorsque le rapport les regroupe). Des pages lourdes sont des pages lentes.
Fuite depuis d'autres locales : C'est le contenu de la même page mais dans une autre langue qui serait chargé par erreur dans la page auditée. Ce contenu est inutile et devrait être évité (ex: contenu de la page
/fr/aboutdans le bundle de la page/en/about).Fuite depuis d'autres routes : La même idée pour les autres écrans de l'application : si leur texte accompagne le chargement alors que vous n'avez ouvert qu'une seule page (ex: contenu de la page
/en/aboutdans le bundle de la page/en/contact). Un score élevé suggère un mauvais découpage ou des bundles trop larges.Taille moyenne de bundle par composant : Les éléments d'interface communs sont mesurés un par un au lieu de se cacher derrière un gros chiffre global de l'application. Cela montre si l'internationalisation gonfle silencieusement les composants quotidiens. Par exemple, si votre composant re-rend, il chargera toutes ces données depuis la mémoire. Attacher un JSON géant à n'importe quel composant revient à connecter un grand réservoir de données inutilisées qui ralentira la performance de vos composants.
Réactivité du changement de langue : Je change la langue en utilisant le propre contrôle de l'application et je mesure le temps nécessaire jusqu'à ce que la page ait clairement basculé - ce qu'un visiteur remarquerait, et non une micro-étape de laboratoire.
Travail de rendu après un changement de langue : Un suivi plus précis : quel effort l'interface a fourni pour se redessiner dans la nouvelle langue une fois le basculement lancé. Utile lorsque le ressenti utilisateur et le coût du framework divergent.
Temps de chargement initial de la page : De la navigation jusqu'à ce que le navigateur considère la page comme entièrement chargée pour les scénarios testés. Idéal pour comparer les démarrages à froid (cold starts).
Temps d'hydratation : Lorsque l'application le permet, combien de temps le client passe à transformer le HTML serveur en quelque chose sur lequel on peut réellement cliquer. Un tiret dans les tableaux signifie que cette implémentation n'a pas fourni de valeur d'hydratation fiable dans ce benchmark.
Étoiles GitHub
Les étoiles GitHub sont un indicateur fort de la popularité d'un projet, de la confiance de la communauté et de sa pertinence à long terme. Bien qu'elles ne soient pas une mesure directe de la qualité technique, elles reflètent le nombre de développeurs qui trouvent le projet utile, suivent ses progrès et sont susceptibles de l'adopter. Pour estimer la valeur d'un projet, les étoiles aident à comparer l'attraction entre les alternatives et fournissent des informations sur la croissance de l'écosystème.
Résultats détaillés
1 - Solutions à éviter
Certaines solutions, telles que gt-next ou lingo.dev, sont clairement à éviter. Elles combinent un verrouillage propriétaire (vendor lock-in) avec une pollution de votre base de code. Malgré de nombreuses heures passées à essayer de les implémenter, je n'ai jamais réussi à les faire fonctionner correctement - ni sur TanStack Start, ni sur Next.js.
Problèmes rencontrés :
(General Translation) ([email protected]) :
- Pour une application de 110 Ko,
gt-nextajoute plus de 440 Ko supplémentaires. Quota Exceeded, please upgrade your plandès le tout premier build avec General Translation.- Les traductions ne sont pas rendues ; j'obtiens l'erreur
Error: <T> used on the client-side outside of <GTProvider>, ce qui semble être un bug de la bibliothèque. - Lors de l'implémentation de gt-next, je suis également tombé sur un problème avec la bibliothèque :
does not provide an export named 'printAST' - @formatjs/icu-messageformat-parser, ce qui cassait l'application. Après avoir signalé ce problème, le mainteneur l'a corrigé sous 24 heures. - La bibliothèque bloque le rendu statique des pages Next.js.
(Lingo.dev) (@lingo.dev/[email protected]) :
- Quota AI dépassé, bloquant entièrement le build - vous ne pouvez donc pas mettre en production sans payer.
- Le compilateur ratait presque 40 % du contenu traduit. J'ai dû réécrire tous les
.mapen blocs de composants plats pour que cela fonctionne. - Leur CLI est buggée et réinitialisait régulièrement le fichier de configuration sans raison.
- Au build, il effaçait totalement les JSONs générés lorsque du nouveau contenu était ajouté. Résultat : une poignée de clés pouvait rayer de la carte plus de 300 clés existantes.
2 - Solutions expérimentales
(Wuchale) ([email protected]) :
L'idée derrière Wuchale est intéressante mais pas encore viable. J'ai rencontré des problèmes de réactivité et j'ai dû forcer le re-rendu du provider pour faire fonctionner l'application. La documentation est également assez floue, ce qui rend l'adoption difficile.
(Paraglide) (@inlang/[email protected]) :
Paraglide propose une approche innovante et bien pensée. Pourtant, dans ce benchmark, le tree-shaking annoncé n'a pas fonctionné pour mes configurations Next.js ou TanStack Start. Le workflow et la DX sont plus complexes que d'autres options.
Personnellement, je n'aime pas devoir régénérer des fichiers JS avant chaque push, ce qui crée un risque constant de conflit de fusion via les PRs. L'outil semble également plus axé sur Vite que sur Next.js.
Enfin, par rapport aux autres solutions, Paraglide n'utilise pas de "store" (ex: contexte React) pour récupérer la langue actuelle afin de rendre le contenu. Pour chaque nœud analysé, il demandera la langue au localStorage / cookie etc. Cela conduit à l'exécution d'une logique inutile qui impacte la réactivité des composants.
Note sur paraglide : la solution injecte du code dans votre base de code à importer, par conséquent la métrique 'taille de la lib' dans le rapport de benchmark est presque de 0. La génération de code est une bonne chose, car la fonction utilisée n'inclura que la logique nécessaire (préfixe total vs pas de préfixe, cookie vs stockage etc). En comparaison, Intlayer procède à ce filtrage via des injections de variables d'environnement dans le build pour forcer le bundler à tree-shaker le contenu selon la logique. Grâce à cela, paraglide et intlayer finissent par être des solutions 6 à 10 fois plus légères que i18next ou next-intl.
3 - Solutions acceptables
(Tolgee) (@tolgee/[email protected]) :
Tolgee traite bon nombre des problèmes mentionnés plus haut. Je l'ai trouvé plus difficile à adopter que des outils similaires. Il n'offre pas de sécurité de type (type safety), ce qui rend également plus difficile la détection des clés manquantes à la compilation. J'ai dû wrapper les fonctions de Tolgee avec les miennes pour ajouter la détection des clés manquantes.
(Next Intl) ([email protected]) :
next-intl est l'option la plus à la mode et celle que les agents IA poussent le plus, mais à mon avis à tort. La mise en route est facile. En pratique, l'optimisation pour limiter les fuites est complexe. Combiner chargement dynamique + namespacing + types TypeScript ralentit beaucoup le développement. Le package est également assez lourd (env. 13 Ko pour NextIntlClientProvider + useTranslations, soit plus de 2 fois next-intlayer). next-intl bloquait auparavant le rendu statique des pages Next.js. Il fournit un helper nommé setRequestLocale(). Cela semble partiellement résolu pour les fichiers centralisés comme en.json / fr.json, mais le rendu statique casse toujours lorsque le contenu est divisé en namespaces tels que en/shared.json / fr/shared.json / es/shared.json.
(Next I18next) ([email protected]) :
next-i18next est probablement l'option la plus populaire car elle fut l'une des premières solutions i18n pour les applications JavaScript. Elle dispose de nombreux plugins communautaires. Elle partage les mêmes inconvénients majeurs que next-intl. Le package est particulièrement lourd (env. 18 Ko pour I18nProvider + useTranslation, environ 3 fois next-intlayer).
Les formats de messages diffèrent également : next-intl utilise ICU MessageFormat, tandis qu'i18next utilise son propre format.
(Next International) ([email protected]) :
next-international s'attaque également aux problèmes ci-dessus mais ne diffère pas beaucoup de next-intl ou next-i18next. Il inclut scopedT() pour les traductions spécifiques à un namespace, mais son utilisation n'a pratiquement aucun impact sur la taille du bundle.
(Lingui) (@lingui/[email protected]) :
Lingui est souvent vanté. Personnellement, j'ai trouvé le workflow lingui extract / lingui compile plus complexe que les alternatives, sans avantage clair. J'ai également remarqué des syntaxes inconsistantes qui perturbent les IAs (ex: t(), t'', i18n.t(), <Trans>).
4 - Recommandations
(Next Translate) ([email protected]) :
next-translate est ma recommandation principale si vous aimez une API de style t(). C'est élégant via next-translate-plugin, chargeant les namespaces via getStaticProps avec un loader Webpack / Turbopack. C'est aussi l'option la plus légère ici (env. 2,5 Ko). Pour le découpage en namespaces, la définition par page ou par route dans la config est bien pensée et plus facile à maintenir que les alternatives principales comme next-intl ou next-i18next. Dans la version 3.1.2, j'ai noté que le rendu statique ne fonctionnait pas ; Next.js se repliait sur le rendu dynamique.
(Intlayer) ([email protected]) :
Je ne jugerai pas personnellement next-intlayer par souci d'objectivité, puisqu'il s'agit de ma propre solution.
Note personnelle
Cette note est personnelle et n'affecte pas les résultats du benchmark. Dans le monde de l'i18n, on voit souvent un consensus autour de const t = useTranslation('xx') + <>{t('xx.xx')}</>.
Dans les applications React, injecter une fonction en tant que ReactNode est, à mon avis, un anti-pattern. Cela ajoute également une complexité évitable et un surcoût d'exécution JavaScript (même s'il est à peine perceptible).