De Bridge à JSI : décider quand migrer vers la nouvelle architecture de React Native
Comment JSI, Fabric et TurboModules remplacent l’ancien pont React Native, ce que signifient en pratique les gains rapportés, et comment auditer et migrer une application existante en toute sécurité.
Sources: React Native’s New Architecture Finally Stopped Being a Compromise
Tout ceux qui ont expédié une application React Native complexe sous l’architecture ancienne ont constaté que le défilement ralentissait brusquement dès que le thread JavaScript devenait occupé par une autre tâche, comme l’analyse d’une réponse réseau ou la propagation d’un changement d’état à travers un grand arbre de composants. Ce ralentissement n’était pas dû au hasard ; c’était la conséquence directe du fonctionnement de l’ancien pont. La Nouvelle Architecture qui le remplace est désormais la configuration par défaut depuis React Native 0.76, mais pendant longtemps de nombreuses équipes l’ont considérée comme quelque chose à adopter plus tard. Cet article explique quels sont les trois éléments modifiés par la Nouvelle Architecture, comment interpréter les affirmations relatives à ses performances, et comment décider s’il convient de migrer une application existante et comment le faire.
Pourquoi l’ancien pont provoquait des ralentissements
Dans l’architecture traditionnelle, toute interaction entre JavaScript et le code natif passait par un pont asynchrone. Une requête était sérialisée en JSON, mise en file d’attente sur le pont, désérialisée du côté natif où elle était exécutée, les résultats revenant ensuite de la même manière. Deux conséquences découlent de cette conception : chaque passage par le pont entraînait des coûts de sérialisation, et comme le pont était asynchrone et ordonné, tout ce qui devait être traité par le code natif devait attendre tant que la thread JavaScript était occupée. Une thread JavaScript chargée bloquait donc le défilement, l’affichage et les gestes qui en dépendaient.
Cette alternative existe déjà sous une forme ou une autre depuis des années, et elle est officiellement devenue la valeur par défaut à partir de 0.76. Cependant, être la valeur par défaut et être quelque chose sur quoi une équipe prudente serait prête à miser pour une application en production sont deux choses distinctes. Les versions sorties au premier semestre de cette année, React Native 0.85 suivi d’Expo SDK 56, sont généralement considérées comme le moment où cette lacune a été comblée ; les détails sont abordés plus loin.
Les trois composants de la Nouvelle Architecture
En mettant de côté le marketing, la Nouvelle Architecture se compose de trois composants qui fonctionnent ensemble, chacun remplaçant une partie spécifique du « pont » entre les technologies.
JSI : appels directs plutôt que messages
L’Interface JavaScript (JSI) constitue la base. Il s’agit d’une interface légère en C++ qui permet à JavaScript de conserver des références vers des objets natifs et d’appeler directement des fonctions natives, y compris de manière synchrone lorsque cela est pertinent. Il n’y a pas de sérialisation JSON, ni de file d’attente de pont, ni de trajet asynchrone pour les appels qui n’en ont pas besoin. L’ancienne procédure de sérialisation, d’enregistrement en file d’attente, de désérialisation et d’envoi se réduit essentiellement à une simple appel de fonction en C++.
Fabric : un arbre d’ombre partagé en C++
Fabric est le système de rendu basé sur JSI. Il organise le rendu en trois couches : l’arbre de fibres React en JavaScript, un arbre d’ombre, et l’arbre des vues natives du hôte. La différence cruciale réside dans le fait que l’arbre d’ombre se trouve désormais dans du code C++ partagé, au lieu d’être géré via le pont depuis JavaScript. Le calcul de la mise en page avec Yoga s’effectue dans cette couche partagée, de sorte que les informations de mise en page ne doivent plus être transmises par le pont.
Fabric est également ce qui permet aux fonctionnalités React concurrentes, telles que les transitions, Suspense et useTransition, de fonctionner sur React Native. Sa phase d’application peut s’exécuter de manière synchrone ou concurrente, et le rendeur basé sur un pont ne pouvait pas prendre en charge correctement aucun de ces modes. Si votre équipe souhaite le même modèle de rendu concurrent qu’au web, Fabric est une condition préalable et non une simple optimisation.
TurboModules : modules natifs différés et typés
TurboModules remplace le système obsolète NativeModules pour accéder aux fonctionnalités natives. Ce fragment met en contraste les deux approches. L’ancienne méthode d’importation récupère un module depuis NativeModules, qui étaient initialisés dès le démarrage et entraînaient des coûts de sérialisation à chaque appel. La nouvelle méthode d’importation charge un module généré à partir d’une spécification typée, soutenu par JSI, initialisé uniquement lors de sa première utilisation et pouvant être appelé de manière synchrone lorsque c’est approprié.
// Old NativeModules: loaded eagerly at startup, whether used or not,
// with every call paying the bridge's serialization cost
import { NativeModules } from 'react-native';
const { CameraModule } = NativeModules;
// TurboModules: JSI-backed, lazily loaded only when actually called,
// with type-safe interfaces generated by Codegen from your spec
import CameraModule from './specs/NativeCameraModule';
CameraModule.capturePhoto(); // direct, synchronous-capable call via JSI
Le chargement différé est plus important qu’il n’y paraît au premier abord. Dans le système ancien, une application dotée de nombreux modules natifs devait supporter un coût d’initialisation à chaque démarrage pour chacun d’eux, même si une session donnée n’utilisait jamais la plupart d’entre eux. TurboModules ne génère de coût que lorsque un module est réellement utilisé. La spécification typée constitue l’autre aspect avantageux : Codegen la transforme en interfaces natives, ce qui est expliqué en détail dans notre article sur pourquoi Codegen rend la spécification en premier obligatoire pour les TurboModules.
Lire attentivement les affirmations relatives aux performances
Les chiffres cités pour la nouvelle architecture varient en fonction de la source et du type de charge de travail, il convient donc de les considérer comme une orientation plutôt que comme des valeurs concrètes à promettre aux parties prenantes. Les résultats rapportés incluent :
- une amélioration d’environ 10 à 30 % de la performance du thread UI pour les applications typiques, pouvant atteindre jusqu’à 3 fois plus pour celles qui reposent fortement sur des modules natifs
- des appels de JavaScript vers le code natif 20 à 50 % plus rapides
- des temps de démarrage 20 à 40 % plus courts, en fonction du nombre de modules natifs présents dans l’application et des opérations mesurées
Un indicateur plus utile que n’importe quel pourcentage est celui qui montre quels types d’applications sont passés du statut « possible avec des solutions de contournement » à celui d’« application fonctionnant directement ». Les applications axées sur la caméra, les visualisations de données en temps réel, les interfaces animées complexes, ainsi que tout ce qui nécessite une mesure de mise en page véritablement synchrone, exigeaient soit des solutions architecturales complexes, soit n’étaient tout simplement pas praticables. Avec la nouvelle architecture, il s’agit de tâches d’ingénierie ordinaires. Si votre application appartient à l’une de ces catégories, les raisons de migrer sont solides, quel que soit le chiffre moyen ; si elle se compose principalement de formulaires et de listes, les avantages seront réels mais plus modestes. Les seuls chiffres qui devraient guider votre décision sont ceux que vous mesurez dans votre propre application avant et après la migration.
Ce que le cycle de publication récent a corrigé
La raison pour laquelle cette année est considérée comme un tournant réside non pas dans une nouvelle fonctionnalité, mais dans un ensemble de correctifs visant les problèmes spécifiques qui faisaient attendre les équipes prudentes. Consultez les notes de version officielles pour obtenir des détails précis, car le résumé ci-dessous reflète les informations disponibles au moment de la rédaction.
- React Native 0.85, sorti en avril, a reconstruit le système d’animation partagé et éliminé une source de problèmes persistants liés à la perte de frames. Il est rapporté que les transitions nécessitant beaucoup d’animations maintiennent 60 fps sur la plupart des appareils pris en charge, sans dégradation sous charge.
Migration en pratique : l’audit des dépendances d’abord
Tout cela ne signifie pas pour autant que chaque application existante doit être migrée dès demain. Le critère à vérifier avant de valider des changements est la compatibilité avec les bibliothèques. Au moment où cet article est écrit, on indique que près de 85 % des paquets npm populaires de React Native prennent en charge la Nouvelle Architecture, ce qui laisse environ 15 % de paquets ne fonctionnant qu’avec des solutions de pont ou étant seulement partiellement compatibles. Les migrations bloquées se retrouvent presque toujours dans cette catégorie : non pas dans le framework lui-même, mais dans quelques modules natifs tiers dont l’application dépend.
Un schéma récurrent chez les équipes qui migrent avec succès se présente comme suit :
- Faites un audit avant de commencer. Énumérez toutes les dépendances natives et vérifiez leur compatibilité avec la nouvelle architecture en vous basant sur leur version actuelle, et non sur ce dont vous vous souvenez d’il y a un ou deux ans. Pour chaque package non pris en charge, décidez s’il faut le mettre à jour, le remplacer, créer une branche dédiée ou le gérer vous-même.
- Utilisez Codegen. Comme Codegen génère des interfaces natives sécurisées du point de vue des types à partir de vos spécifications TypeScript ou JavaScript, toute incohérence entre le code JavaScript et le code natif est détectée au moment de la compilation, plutôt que de provoquer des plantages en temps de exécution. Si vous écrivez vos propres modules, notre guide sur la mise en place d’un Turbo Module intégré de bout en bout avec Codegen illustre l’ensemble du processus.
Guidance selon la situation
Démarrer un nouveau projet
Il y a peu de raisons de choisir une autre approche. La Nouvelle Architecture est la valeur par défaut, les outils associés, en particulier l’Expo SDK 56, partent du principe que vous l’utilisez, et l’écart de compatibilité est désormais suffisamment faible pour que vous ne rencontriez presque jamais de problèmes, à moins que l’application n’aie des dépendances natives inhabituelles.
Gérer une application sur le pont hérité
Commencez par un audit de compatibilité avant d’intervenir sur quoi que ce soit d’autre, en utilisant votre liste réelle des dépendances. La proportion de paquets compatibles augmente constamment, donc une évaluation réalisée il y a un an est probablement obsolète. Planifiez une migration progressive et considérez les erreurs détectées par Codegen comme un véritable filet de sécurité pendant la transition, et non comme une simple formalité.
Équipes qui l’ont évalué précédemment et ont décidé d’attendre
Reporter à 2023 ou 2024 était une décision raisonnable compte tenu des défauts de l’époque. La refonte de l’animation et les correctifs des fuites mémoire dans la version 0.85 ciblent précisément les problèmes de stabilité en production qui justifiaient l’attente, ce qui fait de ce cycle de publication le moment idéal pour reprendre cette question.
Points clés
- Le pont hérité serialisait chaque appel et le mettait en file d’attente derrière un thread JavaScript occupé ; c’est là, et non par malchance, que provenait une grande partie des problèmes de performance historiques.
- JSI permet des appels directs, Fabric déplace l’arbre d’ombre et le rendu vers du C++ partagé tout en permettant un fonctionnement concurrent de React, et TurboModules se charge de manière différée à partir de spécifications typées.
- Les améliorations en termes de vitesse varient considérablement ; prenez votre décision en fonction de la catégorie de votre application et de vos propres mesures.
- Ce qui a changé récemment, ce sont la stabilité et les outils, pas les fonctionnalités : la fiabilité des animations, le comportement de la mémoire sur de longues sessions et la vitesse de compilation.
- Le risque de migration réside dans les modules natifs tiers, il faut donc d’abord auditer les dépendances, compter sur Codegen et migrer écran ou module par écran.