Réduire les lenteurs TypeScript de i18n

Blog article image cover
Cet article fait suite à un autre, il est conseillé de le lire avant : pourquoi-mon-projet-typescript-est-lent.
Dans le dernier article, nous avons vu comment trouver les parties du code qui prennent le plus de temps lors de la compilation TypeScript.
Dans l'exemple donné, nous avions remarqué que TypeScript prenait plus de 3 secondes pour typechecker une partie de code utilisant la librairie d'internationalisation i18next, (ce qui est assez embêtant quand on doit attendre 3 secondes avant d'avoir l'autocomplétion des clés de traduction).
Nous allons résoudre ce problème en 2 étapes :
  • Faire un script pour générer l'union de toutes les clés de traduction
  • Faire un wrapper autour de l'API de i18next pour utiliser notre propre union pour le type des clés de traduction

Pourquoi générer cette union, i18next peut le faire tout seul non ?

Oui, mais il génère cette union en fusionnant les unions de tous les workspaces i18n. Ici, on en a 8 avec 150 clés dans chaque workspace. Or, c'est la fusion des unions des différents workspaces qui semble prendre le plus de temps :
"[...] For instance, to eliminate redundant members from a union, the elements have to be compared pairwise, which is quadratic. This sort of check might occur when intersecting large unions, where intersecting over each union member can result in enormous types that then need to be reduced." (TypeScript performance wiki)
C'est pour cela que je veux construire cette union avec un script plutôt qu'en fusionnant plusieurs unions avec TypeScript.
Vous pouvez accéder au code de cet exemple ici.

Le script :

const { writeFileSync } = require("fs"); const anotherTest = require("./fr/anotherTest.json"); const hehe = require("./fr/hehe.json"); const login = require("./fr/login.json"); const menu = require("./fr/menu.json"); const mobile = require("./fr/mobile.json"); const sample = require("./fr/sample.json"); const userErrors = require("./fr/userErrors.json"); const common = require("./fr/common.json"); const keys = [ ...Object.keys(anotherTest).map((key) => anotherTest:${key}), ...Object.keys(hehe).map((key) => hehe:${key}), ...Object.keys(login).map((key) => login:${key}), ...Object.keys(menu).map((key) => menu:${key}), ...Object.keys(mobile).map((key) => mobile:${key}), ...Object.keys(sample).map((key) => sample:${key}), ...Object.keys(userErrors).map((key) => userErrors:${key}), ...Object.keys(common).map((key) => common:${key}), ]; const keysText = keys.map((key) => | '${key}').join("\n"); const fileText = export type AllKeysUnion = \n${keysText};\n; const FILE_NAME = "AllKeysUnion.ts"; writeFileSync(FILE_NAME, fileText, "utf8");
Ce script est en 3 étapes :
  • on importe les fichiers JSON qui contiennent les clés qui nous intéressent
  • on formate les informations qui nous intéressent dans une grande chaine de caractère (la variable fileText)
  • on crée le fichier avec fs.writeFile
Cela nous donne un fichier qui exporte AllKeysUnion, qui est une union dont chaque chaine a le format "workspace:clé"
export type AllKeysUnion = | "anotherTest:anotherTest-0" | "anotherTest:anotherTest-1" | "anotherTest:anotherTest-2" ...

Le wrapper :

import { i18n as I18nInterface, TOptions } from "i18next"; import { useTranslation as useTranslationOriginal } from "react-i18next"; import { AllKeysUnion } from "./AllKeysUnion"; export type TFunctionWrapper = ( key: AllKeysUnion, options?: TOptions ) => string; export type UseTranslationResponse = { t: TFunctionWrapper; i18n: I18nInterface; ready: boolean; }; export function useTranslationWrapper(): UseTranslationResponse { const { t, i18n, ready } = useTranslationOriginal(); const myT = (key: AllKeysUnion, options?: TOptions) => t(key, options); return { t: myT, i18n, ready }; }
Dans ce fichier, le plus important est notre wrapper useTranslationWrapper, c'est cette fonction que l'on va utiliser à la place du useTranslation de i18next dans notre code React.
Cette fonction va utiliser le useTranslation original de i18next mais en étant typée avec l'union TypeScript que nous avons générée grâce au script réalisé plus tôt.

Utiliser le wrapper :

Il ne nous reste plus qu'à utiliser ce wrapper dans notre code react :
Pour cela, il suffit de remplacer les appels à useTranslation par des appels à useTranslationWrapper.
Dans App.tsx :
const { t } = useTranslationWrapper();
C'est tout ce dont on a besoin de changer.

Vérifier que les lenteurs ont disparues :

Pour être sûr que tout ce travail n'a pas été vain, nous allons utiliser les mêmes outils que dans l'article précédent pour générer les fichiers de rapport de la compilation avant et après les changements effectués.
Une fois les rapports de compilation générés, il ne nous reste plus qu'à utiliser l'outil @typescript/analyze-trace pour contempler le résultat.
Avant :
Hot Spots └─ Check file /home/mayeul/Projects/tests/test-tsc/src/App.tsx (3700ms) └─ Check deferred node from (line 15, char 9) to (line 16, char 33) (3186ms) └─ Check expression from (line 16, char 10) to (line 16, char 20) (3172ms) └─ Check expression from (line 16, char 21) to (line 16, char 29) (3170ms) └─ Check expression from (line 16, char 22) to (line 16, char 28) (3170ms)
Après :
Found nothing in 1 project(s)
L'outil ne détecte plus de code particulièrement lent à compiler, et le projet se compile en moins d'1 seconde, contre plus de 3 secondes avant !
Note : Pour cet article, nous avons fait un wrapper très minimaliste, nous n'avons par exemple pas permis d'obtenir une fonction de traduction relative à un seul workspace, ce qui est possible avec i18next : useTranslation(workspace), cela dit, rien ne nous empêche d'améliorer le wrapper.
Vous souhaitez être accompagné pour lancer votre projet digital ?
Déposez votre projet dès maintenant
Article presentation image
Comment utiliser GitLab CI/CD pour améliorer votre flow de développement ?
Lors du développement d'une application, il y a toujours une petite appréhension lors la mise en production. Cette petite ...
Matthieu Locussol
Matthieu Locussol
Full-Stack Developer @ Galadrim
Article presentation image
Comment changer de version de Node.js avec NVM ?
Vous voulez changer rapidement de version de `node` ? nvm est l’outil qu’il vous faut. Pourquoi nvm ? `node` est un exécutable. ...
Florian Yusuf Ali
Florian Yusuf Ali
Full-Stack Developer @ Galadrim
Article presentation image
Next.js App Router : le cache et ses dangers
“Il y a seulement 2 problèmes compliqués en informatique : nommer les choses, et l’invalidation de cache”. Phil Karlton. Avec ...
Valentin Gerest
Valentin Gerest
Full-Stack Developer @ Galadrim