React est une bibliothèque JavaScript frontale gratuite et open-source pour la construction d'interfaces utilisateur basées sur des composants. Elle est maintenue par Meta (anciennement Facebook) et une communauté de développeurs individuels et d'entreprises.
« React Compiler optimise automatiquement vos composants et hooks, de sorte que seules les parties minimales de votre interface utilisateur se mettent à jour en fonction des changements d'état », a expliqué M. Savona au public présent lors de la React Conference 2024. « Cela semble donc assez magique ».
Objectifs de React Compiler
L'idée de React Compiler est de permettre aux développeurs d'utiliser le modèle de programmation déclaratif familier de React, basé sur les composants, tout en garantissant que les apps sont rapides par défaut. Concrètement, les objectifs à atteindre sont les suivants :
- Limiter la quantité de re-rendu lors des mises à jour afin de garantir des performances rapides et prévisibles par défaut pour les applications.
- Maintenir un temps de démarrage neutre par rapport aux performances antérieures à l'utilisation du compilateur React. Cela signifie notamment que l'augmentation de la taille du code et les frais généraux de mémorisation doivent être suffisamment bas pour ne pas avoir d'impact sur le démarrage.
- Conserver le modèle de programmation déclaratif et orienté composant de React. En d'autres termes, la solution ne devrait pas changer fondamentalement la façon dont les développeurs pensent à écrire React, et devrait généralement supprimer des concepts (le besoin d'utiliser React.memo(), useMemo(), et useCallback()) plutôt que d'en introduire de nouveaux.
- Travailler sur du code React idiomatique qui suit les règles de React (fonctions de rendu pures, règles de hooks, etc.).
- Prendre en charge les outils et flux de travail typiques de débogage et de profilage.
- Être suffisamment prévisible et compréhensible par les développeurs React - c'est-à-dire que les développeurs devraient être en mesure de développer rapidement une intuition approximative de la façon dont React Compiler fonctionne.
- Ne pas nécessiter d'annotations explicites (types ou autres) pour le code produit typique. Des fonctionnalités permettant aux développeurs d'utiliser les informations de type pour permettre des optimisations supplémentaires peuvent être proposées, mais le compilateur doit fonctionner correctement sans informations de type ou autres annotations.
Non-objectifs
Les éléments suivants ne sont explicitement pas des objectifs pour React Compiler :
- Fournir un re-rendu parfaitement optimal sans aucun recalcul inutile. Il s'agit d'un non-objectif pour plusieurs raisons :
- Le surcoût d'exécution lié au suivi supplémentaire peut l'emporter sur le coût du recalcul dans de nombreux cas.
- Dans les cas de dépendances conditionnelles, il peut être impossible d'éviter de recalculer certaines ou toutes les instructions.
- La quantité de code peut faire régresser les temps de démarrage, ce qui irait à l'encontre de l'objectif de neutralité des performances de démarrage.
- Soutenir le code qui viole les règles de React. Les règles de React existent pour aider les développeurs à construire des applications robustes et évolutives et forment un contrat qui permet de continuer à améliorer React sans casser les applications. React Compiler dépend de ces règles pour transformer le code en toute sécurité, et les violations des règles briseront donc les optimisations de React Compiler.
- Prendre en charge les anciennes fonctionnalités de React. Notamment, les composants de classe ne seront pas pris en charge en raison de leur état mutable inhérent partagé entre plusieurs méthodes avec des durées de vie et des flux de données complexes.
- Prendre en charge 100 % du langage JavaScript. En particulier, les fonctionnalités rarement utilisées et/ou celles qui sont connues pour être dangereuses ou qui ne peuvent pas être modélisées correctement ne seront pas prises en charge. Par exemple, les classes imbriquées qui capturent des valeurs à partir de leur fermeture sont difficiles à modéliser avec précision en raison de la mutabilité, et eval() n'est pas sûr. La prise en charge de la grande majorité du code JavaScript (et des dialectes TypeScript et Flow) est l'objectif poursuivi.
Principes de conception de React Compiler
De nombreux aspects de la conception découlent naturellement des objectifs susmentionnés :
- La sortie du compilateur doit être un code de haut niveau qui conserve non seulement la sémantique de l'entrée, mais qui est également exprimé à l'aide de constructions similaires à celles de l'entrée. Par exemple, plutôt que de convertir les expressions logiques (a ? ? b) en une instruction if, la forme de haut niveau de l'expression logique est conservée. Plutôt que de convertir toutes les constructions de boucles en une seule forme, la forme originale de la boucle est conservée. Cela découle des objectifs :
- Le code de haut niveau est plus compact et permet de réduire l'impact de la compilation sur la taille de l'application.
- Les constructions de haut niveau qui correspondent à ce que le développeur a écrit sont plus faciles à déboguer.
- Il s'ensuit que la représentation interne du compilateur doit également être de haut niveau pour pouvoir restituer les constructions originales de haut niveau. La représentation interne est ce qui a été appelé une représentation intermédiaire de haut niveau (HIR) - un nom emprunté au compilateur Rust. Cependant, la HIR de React Compiler est peut-être encore plus adaptée à ce nom, car elle conserve des informations de haut niveau (distinguant if vs logical vs ternary, ou for vs while vs for..of) mais représente également le code comme un graphe de flux de contrôle sans imbrication.
Architecture de React Compiler
React Compiler a deux interfaces publiques principales : un plugin Babel pour transformer le code, et un plugin ESLint pour signaler les violations des règles de React. En interne, les deux utilisent la même logique de base du compilateur.
Le cœur du compilateur est largement découplé de Babel, utilisant ses propres représentations intermédiaires. Le flux de haut niveau est le suivant :
- Plugin Babel : Détermine quelles fonctions d'un fichier doivent être compilées, sur la base des options du plugin et de toute directive locale opt-in/opt-out. Pour chaque composant ou hook à compiler, le plugin appelle le compilateur, en passant la fonction originale et en obtenant en retour un nouveau nœud AST qui remplacera l'original.
- Réduction (BuildHIR) : La première étape du compilateur consiste à convertir l'AST de Babel en la principale représentation intermédiaire de React Compiler, HIR (High-level Intermediate Representation). Cette phase est principalement basée sur l'AST lui-même, mais s'appuie actuellement sur Babel pour résoudre les identifiants. La HIR préserve la sémantique précise de l'ordre d'évaluation de JavaScript, résout les ruptures/continuités à leurs points de saut, etc. La HIR qui en résulte forme un graphe de flux de contrôle de blocs de base, dont chacun contient zéro ou plusieurs instructions consécutives suivies d'un terminal. Les blocs de base sont stockés dans l'ordre inverse, de sorte que l'itération vers l'avant des blocs permet aux prédécesseurs d'être visités avant les successeurs, à moins qu'il n'y ait un « back edge » (c'est-à-dire une boucle).
- Conversion SSA (EnterSSA) : La HIR est converti en forme HIR, de sorte que tous les identifiants de la HIR sont mis à jour avec un identifiant basé sur le SSA.
- Validation : Plusieurs passes de validation sont exécutées pour vérifier que l'entrée est valide pour React, c'est-à-dire qu'elle n'enfreint pas les règles. Cela inclut la recherche d'appels de hook conditionnels, d'appels setState inconditionnels, etc.
- Optimisation : Diverses passes telles que l'élimination du code mort et la propagation des constantes peuvent généralement améliorer les performances et réduire la quantité d'instructions à optimiser ultérieurement.
- Inférence de type (InferTypes) : Une passe d'inférence de type conservatrice est exécutée pour identifier certains types de données clés susceptibles d'apparaître dans le programme et qui sont pertinents pour une analyse plus approfondie, comme les valeurs qui sont des hooks, les primitives, etc.
- Inférence des portées réactives (reactive scopes) : Plusieurs passages sont nécessaires pour déterminer les groupes de valeurs qui sont créés/mutés ensemble et l'ensemble des instructions impliquées dans la création/mutation de ces valeurs. Ces groupes sont appelés « portées réactives » et chacun d'entre eux peut comporter une ou plusieurs déclarations (ou parfois une réaffectation).
- Construction/optimisation des portées réactives : Une fois que le compilateur a déterminé l'ensemble des portées réactives, il transforme le programme pour rendre ces portées explicites dans la HIR. Le code est ensuite converti en une ReactiveFunction, qui est un hybride du HIR et d'un AST. Les champs d'application sont encore élagués et transformés. Par exemple, le compilateur ne peut pas rendre les appels de hook conditionnels, donc toutes les portées réactives qui contiennent un appel de hook doivent être élaguées. Si deux scopes consécutifs seront toujours invalidés ensemble, ils seront fusionnés pour réduire la charge de travail, etc.
- Codegen : Enfin, la ReactiveFunction hybride HIR/AST est reconvertie en un nœud AST Babel brut, et renvoyée au plugin Babel.
- Plugin Babel : Le plugin Babel remplace le nœud original par la nouvelle version.
Le plugin ESLint fonctionne de la même manière. Pour l'instant, il invoque effectivement le plugin Babel sur le code et renvoie un sous-ensemble d'erreurs. Le compilateur peut rapporter une variété d'erreurs, y compris que le code est simplement du JavaScript invalide, mais le plugin ESLint filtre pour ne montrer que les erreurs spécifiques à React.
Source : React Compiler (Meta)
Et vous ?
Que pensez-vous de React Compiler et de ses capacités ?
Voir aussi :
État de JavaScript 2022 : React reste le framework front-end dominant mais est en perte de vitesse côté satisfaction, jQuery est la troisième bibliothèque la plus utilisée
React 18 est disponible avec le traitement par lots activé par défaut, de nouvelles API comme startTransition, et la prise en charge de Suspense