Auteur:
    Création:2026-04-20Dernière mise à jour:2026-05-18

    Bibliothèques i18n TanStack Start - Rapport de Benchmark 2026

    Cette page est un rapport de benchmark pour les solutions i18n sur TanStack Start.

    Table des Matières

    Benchmark Interactif

    Référence des résultats :

    intlayer.org
    Voir les données complètes du benchmark

    Voir le dépôt complet du benchmark ici.

    Introduction

    Les solutions d'internationalisation figurent parmi les dépendances les plus lourdes d'une application React. Sur TanStack Start, le risque principal est d'embarquer du contenu inutile : les traductions d'autres pages et d'autres locales dans le bundle d'une seule route.

    À mesure que votre application grandit, ce problème peut rapidement faire exploser la quantité de JavaScript envoyée au client et ralentir la navigation.

    En pratique, pour les implémentations les moins optimisées, une page internationalisée peut finir par être plusieurs fois plus lourde que la version sans i18n.

    L'autre impact concerne l'expérience développeur (DX) : la façon dont vous déclarez le contenu, les types, l'organisation des namespaces, le chargement dynamique et la réactivité lors du changement de langue.

    TL;DR

    • Intlayer : Fournit la meilleure performance et la plus petite taille de bundle (v8.7.12) pour TanStack Start.
    • react-i18next & next-intl : Alternatives matures avec de grands écosystèmes, mais nettement plus lourdes et complexes à optimiser.
    • Paraglide : Idée innovante de tree-shaking qui ne fonctionne pas en pratique. DX complexe et surcoût de réactivité dans TanStack Start.
    • À éviter : General Translation (GT) et Lingo.dev en raison de graves problèmes de performance, des limites de quota AI et du verrouillage propriétaire (vendor lock-in).

    Testez votre application

    Pour repérer rapidement les problèmes de fuite i18n, j'ai mis en place un scanner gratuit disponible ici.

    intlayer.org

    Le problème

    Deux leviers sont essentiels pour limiter le coût d'une application multilingue :

    • Découper le contenu par page / namespace afin de ne pas charger des dictionnaires entiers quand on n'en a pas besoin
    • Charger la bonne locale dynamiquement, uniquement quand nécessaire

    Comprendre les limitations techniques de ces approches :

    Chargement dynamique

    Sans chargement dynamique, la plupart des solutions gardent les messages en mémoire dès le premier rendu, ce qui ajoute un surcoût important pour les applications ayant beaucoup de routes et de langues.

    Avec le chargement dynamique, vous acceptez un compromis : moins de JS initial, mais parfois une requête supplémentaire lors du changement de langue.

    Découpage du contenu (Splitting)

    Les syntaxes basées sur const t = useTranslation() + t('a.b.c') sont très pratiques mais encouragent souvent la conservation de gros objets JSON au runtime. Ce modèle rend le tree-shaking difficile à moins que la bibliothèque ne propose une réelle stratégie de découpage par page.

    Méthodologie

    Pour ce benchmark, nous avons comparé les bibliothèques suivantes :

    • Base App (Pas de bibliothèque i18n)
    • react-intlayer (v8.7.12)
    • react-i18next (v17.0.2)
    • next-intl (v4.9.1)
    • @lingui/core (v5.3.0)
    • @inlang/paraglide-js (v2.15.1)
    • @tolgee/react (v7.0.0)
    • react-intl (v10.1.1)
    • wuchale (v0.22.11)
    • gt-react (vlatest)
    • lingo.dev (v0.133.9)

    Le framework utilisé est TanStack Start avec une application multilingue de 10 pages et 10 langues.

    Nous avons comparé quatre stratégies de chargement :

    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 exécuté la même application multilingue dans un vrai navigateur pour chaque stack, puis j'ai noté ce qui s'est réellement affiché sur le réseau et combien de temps cela a pris. Les tailles sont rapportées après une compression web normale, car cela se rapproche davantage de ce que les gens téléchargent que des nombres de sources brutes.

    • 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 (par ex. NextIntlClientProvider) + hooks (par ex. useTranslations) code dans un composant vide. Cela n'inclut pas le chargement des fichiers de traduction. Cela répond à la question de savoir combien coûte la bibliothèque avant que votre contenu n'entre en jeu.

    • JavaScript par page : Pour chaque route de benchmark, combien de script le navigateur télécharge lors de cette visite, en moyenne sur les pages de la suite (et sur les locales où le rapport les agrège). Les pages lourdes sont des pages lentes.

    • Fuite 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 doit être évité. (par ex. contenu de la page /fr/about dans le bundle de la page /en/about)

    • Fuite d'autres routes : La même idée pour les autres écrans de l'application : si leur copie est chargée lorsque vous n'avez ouvert qu'une seule page. (par ex. contenu de la page /en/about dans le bundle de la page /en/contact). Un score élevé suggère un fractionnement faible ou des bundles trop larges.

    • Taille moyenne du bundle de composant : Les éléments d'interface utilisateur courants sont mesurés un à la fois au lieu de se cacher dans un énorme nombre d'applications. Cela montre si l'internationalisation augmente discrètement les composants ordinaires. Par exemple, si votre composant se re-render, il chargera toutes ces données depuis la mémoire. Attacher un énorme JSON à n'importe quel composant, c'est comme connecter un grand magasin de données inutilisées qui ralentira les performances de vos composants.

    • Réactivité du changement de langue : Je bascule la langue en utilisant le contrôle de l'application et je mesure le temps qu'il faut jusqu'à ce que la page ait clairement basculé, ce qu'un visiteur remarquerait, pas une micro-étape de laboratoire.

    • Travail de rendu après un changement de langue : Un suivi plus étroit : combien d'effort l'interface a dû fournir pour se redessiner dans la nouvelle langue une fois le changement en vol. Utile lorsque le temps « ressenti » et le coût du framework divergent.

    • Temps de chargement initial de la page : De la navigation à la considération par le navigateur de la page comme entièrement chargée pour les scénarios que j'ai testés. Bon pour comparer les démarrages à froid.

    • Temps d'hydratation : Quand l'application l'expose, combien de temps le client passe à transformer du HTML serveur en quelque chose sur lequel vous pouvez réellement cliquer. Un tiret dans les tableaux signifie que cette implémentation n'a pas fourni de chiffre 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.

    Star History Chart

    Résultats détaillés

    1 - Solutions à éviter

    Certaines solutions, telles que gt-react ou lingo.dev, sont clairement à fuir. Elles combinent un verrouillage propriétaire avec une pollution de votre base de code. Pire : malgré de nombreuses heures passées à essayer de les implémenter, je n'ai jamais réussi à les faire fonctionner correctement sur TanStack Start (comme pour Next.js avec gt-next).

    Problèmes rencontrés :

    (General Translation) (gt-react@latest) :

    • Pour une application d'environ 110 Ko, gt-react peut ajouter plus de 440 Ko supplémentaires (ordre de grandeur observé sur l'implémentation Next.js du même benchmark).
    • Quota Exceeded, please upgrade your plan dè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-tanstack-start-react, 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.
    • Ces bibliothèques utilisent un anti-pattern via la fonction initializeGT(), empêchant le bundle d'être tree-shaké proprement.

    (Lingo.dev) (lingo.dev@0.133.9) :

    • Quota AI dépassé (ou dépendance serveur bloquante), rendant le build / la production risqués sans payer.
    • Le compilateur ratait presque 40 % du contenu traduit. J'ai dû réécrire tous les .map en blocs de composants plats pour que cela fonctionne.
    • Leur CLI est buggée et réinitialisait le fichier de config sans raison.
    • Au build, il effaçait totalement les JSONs générés quand il y avait du nouveau contenu. Résultat : vous pouviez vous retrouver avec seulement quelques clés effaçant des centaines de clés existantes.
    • J'ai rencontré des problèmes de réactivité avec la bibliothèque sur TanStack Start : au changement de langue, je devais forcer le re-rendu du provider pour que cela fonctionne.

    2 - Solutions expérimentales

    (Wuchale) (wuchale@0.22.11) :

    L'idée derrière Wuchale est intéressante mais ce n'est pas encore une solution viable. J'ai rencontré des problèmes de réactivité avec la bibliothèque et j'ai dû forcer le re-rendu du provider pour faire fonctionner l'application sur TanStack Start. La documentation est également assez floue, ce qui rend l'adoption difficile.

    3 - Solutions acceptables

    (Paraglide) (@inlang/paraglide-js@2.15.1) :

    Paraglide propose une approche innovante et bien pensée. Pourtant, dans ce benchmark, le tree-shaking dont leur entreprise fait la publicité n'a pas fonctionné pour mon implémentation Next.js ou pour TanStack Start. Le workflow et la DX sont également plus complexes d'autres options. Personnellement, je ne suis pas fan de devoir régénérer des fichiers JS avant chaque push, ce qui crée un risque constant de conflit de fusion pour les développeurs via les PRs.

    Note sur paraglide : cette solution injecte du code dans votre base de code pour les imports, par conséquent, la métrique 'lib size' 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 partout vs pas de préfixe, cookie vs stockage, etc.). En comparaison, Intlayer effectue ce filtrage via des injections de variables d'environnement dans le build pour forcer le bundler à tree-shaker le contenu en fonction de la logique. Grâce à cela, paraglide et intlayer finissent par être des solutions 6 à 10 fois plus légères qu'i18next ou next-intl.

    (Tolgee) (@tolgee/react@7.0.0) :

    Tolgee traite bon nombre des problèmes mentionnés plus haut. Je l'ai trouvé plus difficile à prendre en main que d'autres outils aux approches similaires. Il n'offre pas de sécurité de type, ce qui rend également beaucoup plus difficile la détection des clés manquantes à la compilation. J'ai dû wrapper les APIs de Tolgee avec les miennes pour ajouter la détection des clés manquantes.

    Sur TanStack Start, j'ai également eu des problèmes de réactivité : au changement de locale, je devais forcer le provider à se re-rendre et souscrire aux événements de changement de locale pour que le chargement dans une autre langue se comporte correctement.

    (next-intl) (next-intl@4.9.1) :

    next-intl est la pièce "intl" la plus à la mode dans l'écosystème React (même famille que next-intl) et est souvent poussée par les agents IA, mais à mon avis à tort dans un contexte privilégiant la performance. La mise en route est assez simple. En pratique, le processus pour optimiser et limiter les fuites est assez complexe. De même, combiner chargement dynamique + namespacing + types TypeScript ralentit beaucoup le développement.

    Sur TanStack Start, vous évitez les pièges spécifiques à Next.js (setRequestLocale, rendu statique), mais le problème de fond est le même : sans une discipline stricte, le bundle transporte rapidement trop de messages et la maintenance des namespaces par route devient pénible.

    (react-i18next) (react-i18next@17.0.2) :

    react-i18next est probablement l'option la plus populaire car elle fut l'une des premières à servir les besoins i18n des applications JS. Elle dispose également d'un large éventail de plugins communautaires pour des problèmes spécifiques.

    Pourtant, elle partage les mêmes inconvénients majeurs que les stacks basées sur t('a.b.c') : les optimisations sont possibles mais très gourmandes en temps, et les gros projets risquent de mauvaises pratiques (namespaces + chargement dynamique + types).

    Les formats de messages divergent également : next-intl utilise ICU MessageFormat, tandis qu'i18next utilise son propre format - ce qui complique l'outillage ou les migrations si vous les mélangez.

    (Lingui) (@lingui/core@5.3.0) :

    Lingui est souvent loué. Personnellement, j'ai trouvé le workflow autour de lingui extract / lingui compile plus complexe que d'autres approches, sans avantage clair dans ce benchmark TanStack Start. J'ai également remarqué des syntaxes inconsistantes qui perturbent les IAs (ex: t(), t'', i18n.t(), <Trans>).

    (react-intl) (react-intl@10.1.1) :

    react-intl est une implémentation performante de l'équipe Format.js. La DX reste verbeuse : const intl = useIntl() + intl.formatMessage({ id: "xx.xx" }) ajoute de la complexité, du travail JavaScript supplémentaire et lie l'instance globale i18n à de nombreux nœuds dans l'arbre React.

    4 - Recommandations

    Ce benchmark TanStack Start n'a pas d'équivalent direct à next-translate (plugin Next.js + getStaticProps). Pour les équipes qui veulent vraiment une API t() avec un écosystème mature, react-i18next et next-intl restent des choix "raisonnables", mais attendez-vous à investir beaucoup de temps dans l'optimisation pour éviter les fuites.

    (Intlayer) (react-intlayer@8.7.12) :

    Je ne jugerai pas personnellement react-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. Pourtant, dans le monde de l'i18n, on voit souvent un consensus autour d'un pattern comme const t = useTranslation('xx') + <>{t('xx.xx')}</> pour le contenu traduit.

    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).