Fabric dessine le futur de React Native

L’architecture multi-threads de React Native

React Native s’est taillé une place de choix ses dernières années dans le domaine du développement mobile. Initialement créé par Facebook et maintenu par plusieurs centaines de contributeurs, RN reste aujourd’hui une des solutions les plus éprouvées pour réaliser des applications natives avec des technologies de développement Web.

React Native a été conçu sur la base de plusieurs constats. Les périphériques mobiles offrent aujourd’hui des performances de plus en plus élevées. Toujours plus innovants, ils introduisent chaque année de nouvelles fonctionnalités natives telles que les écrans aux bords incurvés, les menus logiciels dynamiques (soft menus), les gestures complexes, etc … Ces innovations se traduisent par l’introduction régulière de nouveaux widgets (navigation, menus, gestures….).

L’autre constat est la fragmentation des technologies mobiles natives. Créer et maintenir une application avec des équipes iOS et Android séparées, une culture différente, des tests dupliqués et surtout un budget double reste un luxe pour un grand nombre d’entreprises.

Dans le même temps, le Web et ses technologies arrivent à maturité (JavaScript, CSS, la notion de composant Web, ….). On ne compte plus le nombre de développeurs disponibles sur le marché et compétents dans ce domaine avec l’avènement du typage dans JavaScript.

React Native apporte une solution à la plupart de ces problèmes. Tout d’abord en s’appuyant sur les technologies Web comme un moyen d’utiliser le rendu graphique du système hôte. A titre d’exemple, le code JavaScript utilise réellement la zone de saisie (TextField) native et non un quelconque composant Web stylé en CSS pour mimer son homologue natif. Toute innovation du Design UI introduite dans un widget iOS ou Android est utilisable quasi instantanément via du code JavaScript encapsulant le widget natif associé. RN propose en quelque sorte le plus petit dénominateur commun aux deux plateformes en permettant à tout moment d’utiliser une fonctionnalité spécifique d’Android ou iOS.

Pour mieux comprendre comment cette prouesse technique est possible, il faut comprendre l’architecture technique de React Native. React Native s’appuie sur le Framework React, basé sur le principe de composant et proposant une programmation déclarative (voir JSX). React ne constitue que le socle permettant d’écrire ses composants, ce n’est pas React qui permet de communiquer avec le système mobile mais la brique React Native Bridge.

Concrètement, le code d’une application React Native est confiné dans une machine virtuelle JS dénommée JavaScriptCore. Celle-ci joue le rôle d’intermédiaire entre l’application JS et le mobile. Tous les échanges passent par des communications asynchrones (via WebSocket) entre les différentes briques de la plateforme RN avec 3 principaux threads :

  • UI Thread: C’est le thread principal sous Android ou iOS. Il est responsable du rendu final des widgets.
  • Shadow Thread: La particularité de ce thread est d’effectuer toutes les opérations liées au calcul de l’arbre des composants graphiques : le layout (gestionnaire des placements)
  • JS Thread: C’est le thread hébergé dans la VM JavaScriptCore, celui qui exécute notre code JS React Native.

Le design de React Native s’appuie sur cette notion de séparation des responsabilités via ces 3 Threads. En « asynchronisant » toutes les routines graphiques, les auteurs de React Native ont voulu fournir une interface graphique « réactive », c’est-à-dire sans temps de latence. Toute opération longue de repositionnement de widget dans une vue peut être réalisée en même temps qu’une opération métier JS consommatrice. C’est la philosophie principale de React Native par rapport à ses concurrents. Le Shadow Thread est l’endroit où s’effectue tous les calculs de repositionnement, React Native utilise dans ce thread l’outil Yoga afin de fournir un comportement unifié (Android et iOS) des FlexBox utilisés dans le monde Web. Yoga est écrit en C, c’est une bibliothèque conçue pour être compatible avec plusieurs plateformes (C#, Java, C ou Swift). Un flux hiérarchique Yoga est donc construit et envoyé au Shadow Thread via une chaîne JSON, il est ensuite calculé et ré-envoyé au Thread UI du mobile pour l’opération finale d’affichage.

Cette chaîne complète d’appel constitue le mécanisme de base du rendu React Native.

C’est grâce au RN Bridge que React Native est en mesure d’utiliser les widgets natifs des différents mobiles tout en gérant de manière unifié le placement et les vues.

Les forces de React Native sont aussi ses faiblesses

Dans cette architecture, certaines choses simples deviennent parfois complexes à réaliser. Imaginons un développeur souhaitant tracer et déboguer un clic sur un bouton générant l’affichage d’un texte dans une vue. De part cette nature asynchrone, les piles d’appels ressemblent généralement à de vrais hiéroglyphes. Le clic généré envoyant un message Yoga dans une file asynchrone transmettant lui-même un arbre UI au système hôte.

L’autre aspect, plus gênant, du React Native Bridge est son utilisation dans un mode où l’interface graphique souhaite un rafraîchissement instantané suite à l’action d’un utilisateur. C’est le cas notamment des scrollbars ou des animations devant rafraîchir leur contenu au même rythme que les mouvements de la souris ou une gesture nécessitant un affichage de la région concernée. Avec RN Bridge, rien n’est instantané car tout est asynchrone, il est difficile d’ordonnancer les routines graphiques, de prioriser/bloquer certaines tâches. Une liste infinie va ainsi continuer de recevoir des événements d’une page qui n’est plus à l’écran et dont on ne souhaite plus l’affichage.

Ce fameux bridge a été créé comme frontière entre le monde Web et le mode natif, c’est de cette manière qu’il est possible d’exécuter du code JavaScript qui utilise des widgets natifs. Mais ce qui fait la force de React Native devient dans ces cas précis une vraie faiblesse.

C++ à la rescousse

C’est à partir de ce constat que l’équipe de développement a commencé à étudier une manière plus directe de communiquer avec le système hôte. Certains se sont aperçus que cette problématique existait déjà dans les navigateurs actuels. En effet, lorsqu’on affiche une page Web sous Chrome, celle-ci communique bien avec le système hôte (MacOS, Windows, Android, …) en affichant un widget dont l’aspect est celui du système d’exploitation hôte. Les boutons, les cases à cocher et autres composants graphiques primitifs sont utilisés via un lien existant entre JavaScriptCore et les composants natifs par un mécanisme d’appel C++. C’est par ce biais que fonctionne l’ordre console.log(), document.createElement() ou Date.now. Pour s’en rendre compte il suffit d’afficher le contenu de ces fonctions dans une console Chrome. Celle-ci nous affiche pour la date : ƒ now() { [native code] }.

Sans le savoir et depuis plusieurs années, l’équipe React Native disposait sous ses yeux d’une interface C++ dans JavaScriptCore prête à l’emploi et largement utilisée. Il n’en fallait pas plus pour créer « JavaScript Interface » ou JSI. L’équipe React Native va donc largement s’inspirer de ce mécanisme pour la prochaine version de son Framework.

L’architecture Fabric

« Fabric » est le nom de code donné à la prochaine version de React Native qui s’achemine vers le mode 100% synchrone. Plutôt que d’accéder au système natif par le biais du RN Bridge en mode asynchrone, React Native va simplifier les échanges pour qu’ils soient in-process dans le même thread. Cela permettra d’accéder aux widgets natif via des références C++ directes sans sérialisation et désérialisation. C++ devient le langage par excellence permettant de factoriser du code sur iOS et Android avec en prime l’utilisation d’un générateur de code pour réduire la verbosité lié au langage C++. La première conséquence est la refonte complète des actuels gestionnaires d’affichage (UIManager) pour utiliser intensivement C++ dans un modèle synchrone. La seconde est la création d’un nouveau système d’interopérabilité entre le code JS et les modules natifs appelé Turbo Modules. Ces derniers seront le pendant des Native Modules asynchrones actuels sous une forme synchrone. Il sera évidemment toujours possible de réaliser des appels natifs asynchrones via des promesses ou un scheduler, mais l’idée est de rendre la mécanique interne de React Native totalement synchrone.

Avec cette refonte, React Native va perdre du poids car des briques essentielles de son architecture vont être migrées en C++ et se rapprocher du système mobile. Plus que jamais le terme « Native » de React Native n’aura pris autant de sens. Les gains sur les performances, eux, devraient être massifs. En revanche, gardons à l’esprit qu’un modèle synchrone apporte également son lot de joyeuseté. Un développeur peut ainsi geler l’interface en cas d’erreur de codage, les crashes seront probablement plus nombreux car la réunification dans un seul et même thread rend également l’application plus monolithique et sensible aux opérations longues de modification d’IHM.

Auteur : Sami JABER (sami.jaber@dng-consulting.com)


Pour plus d’informations :

The New React Native Architecture Explained : https://formidable.com/blog/2019/react-codegen-part-1/

React Native : https://facebook.github.io/react-native/

React Native JSI : https://medium.com/@christian.falch/https-medium-com-christian-falch-react-native-jsi-challenge-1201a69c8fbf

Talk de Parashuram sur l’origine de cette refonte : https://www.youtube.com/watch?v=UcqRXTriUVI

Blog de Parashuram sur Fabric et les TurboModules : http://blog.nparashuram.com/2019/01/react-natives-new-architecture-glossary.html