Relay Modern : Plus simple, plus rapide, plus extensible

Publié le 01 novembre 2017 par Rhw @RevueHW

Aujourd’hui, nous avons Relay Modern, une nouvelle version de Relay conçue dès le départ pour être plus facile à utiliser, plus extensible et, surtout, capable d’améliorer les performances sur les appareils mobiles. Dans cet article, on va voir un bref aperçu de Relay et examiner les nouveautés de Relay Modern.

Réintroduction sur Relay

Relay est le framework JavaScript qui permet de construire des applications fondées sur des données. Il combine React pour les interfaces utilisateur composables et GraphQL pour l’extraction de données composables. Bien qu’il soit possible d’utiliser ces technologies ensemble sans utiliser un cadre applicatif, on a constaté que cette approche n’était pas toujours adaptée à nos besoins. Par exemple, au fur et à mesure que nos applications prenaient de l’ampleur, il devenait plus difficile de gérer les demandes adressées par le réseau, la gestion des erreurs et la cohérence des données, entre autres. Les approches standard peuvent également rompre l’encapsulation du code, ce qui oblige les développeurs à apporter des modifications à travers la base du code afin de réaliser même les petits changements.

Notre objectif avec Relay est de permettre aux développeurs de se déplacer rapidement en se concentrant davantage sur la construction de leurs applications et moins sur la ré-implémentation de ces détails complexes et sources d’erreurs. Pour ce faire, Relay introduit deux concepts : les données colocalisées et les définitions de vue, ainsi que la récupération des données déclaratives.

Colocation des données et vue

Lors de la création des interfaces utilisateur dans React, nous décrivons une application entière comme un ensemble de composants imbriqués. News Feed est une collection d’histoires ; toutefois, une histoire a un en-tête, un corps et une liste de commentaires ; et chaque commentaire a un auteur et un texte ; ainsi de suite. Chacune de ces parties de l’interface utilisateur est décrite par un composant React, permettant aux développeurs de raisonner localement sur leur application. Cette approche permet également la réutilisation : le même composant de l’histoire utilisé dans le fil d’actualité peut être utilisé pour une page de détails d’un article.

De même, les fragments GraphQL sont des unités composables qui permet de décrire les dépendances des données. La requête GraphQL pour le News Feed peut être décomposée en fragments pour les articles ; ce sont les fragments pour l’en-tête, le corps, les commentaires, etc.

En construisant Relay, nous avons réalisé que ces concepts pouvaient être alignés pour former une puissante unité d’abstraction : des conteneurs qui colocalisent à la fois la logique de vue et les dépendances des données de chaque composant. Voici un exemple de conteneur Relay Modern qui affiche le nom et la photo de profil d’un utilisateur et spécifie ses dépendances de données en conséquence :

const ProfilUtilisateur = Relay.createFragmentContainer(
  // View: A React component (functional or class)
  props => {
   const utilisateur = props.data;
   return (
   <View>
   <Text>{utilisateur.nom}</Text>
   <Image src={{uri: utilisateur.photo.uri}} />
   </View>
   );
  },
  // Data: A GraphQL fragment
  // The shape of the fragment matches what is expected in props.
  graphql`
   fragment ProfilUtilisateur on User {
   nom
   photo { uri }
   }
  `
);
   
// Use as a normal React component:
<ProfilUtilisateur data={...} />

Les conteneurs de Relay s’intègrent sans difficulté avec React et GraphQL : ce sont des composants React réguliers qui se composent ensemble comme les développeurs s’attendent, et les fragments GraphQL se composent ensemble en utilisant la syntaxe standard de GraphQL. Depuis l’introduction de données et de vues colocalisées dans Relay, nous avons appliqué cette approche partout où nous utilisons GraphQL sur Facebook, et nous recommandons la colocation à la communauté GraphQL comme bonne pratique.

Récupération des données déclaratives

De la même manière que React permet aux développeurs d’exprimer : qu’est-ce qui devrait être rendu plutôt que comment ? Relay permet aux développeurs de spécifier : quelles données sont requises ? Au lieu de comment les charger, les mettre en cache et les mettre à jour ? En libérant les développeurs de la nécessité de gérer directement le réseau, Relay permet de réduire la complexité des applications et d’éviter une source potentielle de bogues et des problèmes de performance accidentelle.

Un avantage supplémentaire de cette approche est que les améliorations apportées au cadre applicatif peuvent bénéficier à plusieurs applications sans la nécessité de mises à jour par application. Dans l’ensemble, nous avons trouvé que la collecte de données déclaratives était l’une des principales forces de Relay.

Présentation du Relay Modern

Ces deux concepts continuent d’être une partie importante de l’appel de Relay aux développeurs de produits. En fait, alors que nous avions initialement conçu Relay pour nous aider à créer des applications pour les ordinateurs de bureau, les tablettes et les autres appareils haut de gamme, aujourd’hui, nous utilisons Relay dans une grande variété d’applications, allant des outils Web internes très riches aux applications mobiles conçues à partir du React Native. Cependant, comme nous avons utilisé Relay sur une plus large gamme de périphériques – en particulier le matériel mobile sous-alimenté – nous avons réalisé certaines limitations avec la conception originale. En particulier, la nature expressive et flexible de l’API a rendu difficile l’atteinte du niveau de performance souhaité sur certains appareils.

En même temps, nous avons écouté attentivement les commentaires des développeurs internes et de la communauté. Ils ont indiqué que l’API était trop « magique », ce qui rendait parfois difficile l’apprentissage et la prédiction. Nous avons réalisé qu’une approche plus simple pourrait être plus facile à comprendre et permettre également d’obtenir de meilleures performances. Alors, nous avons donc décidé de reconcevoir Relay pour prendre en compte ces nouvelles contraintes, en nous inspirant de nos applications GraphQL mobiles natives existantes.

Le résultat est Relay Modern, un framework GraphQL prévisionnel qui intègre les apprentissages et les meilleures pratiques du Relay classique, de nos clients GraphQL mobiles natifs et de la communauté GraphQL. Relay Modern conserve les meilleures parties de Relay – les données colocalisées et les définitions de vue, la récupération des données déclaratives – tout en simplifiant l’API, en ajoutant des fonctionnalités, en améliorant la performance et en réduisant la taille Du framework. Pour ce faire, nous avons adopté deux concepts : les requêtes statiques et l’optimisation anticipée.

Requêtes statiques

GraphQL a été utilisé dans les applications iOS et Android natives de Facebook depuis 2012. Alors que Relay cherchait à améliorer la performance des appareils moins puissants sur de mauvaises connexions mobiles, nous nous sommes tournés vers ces équipes d’ingénierie pour trouver des sources d’inspiration, notamment en ce qui concerne l’analyse statique et la compilation anticipée.

Les équipes des applications natives ont découvert que l’utilisation de GraphQL s’accompagnait d’un surcoût supplémentaire lié à la création des requêtes en concaténant un tas de chaînes, puis en téléchargeant ces requêtes sur des connexions lentes. Ces requêtes peuvent parfois devenir des dizaines de milliers de lignes de GraphQL. En outre, tous les appareils mobiles exécutant la même application envoyaient en grande partie les mêmes requêtes.

Les équipes ont réalisé que si les requêtes GraphQL étaient statiquement connues, c’est-à-dire qu’elles n’étaient pas altérées par les conditions d’exécution, elles pouvaient être construites une seule fois durant le développement et sauvegardées sur les serveurs Facebook puis remplacées dans l’application mobile par un minuscule identifiant. Avec cette approche, l’application envoie l’identifiant avec certaines variables GraphQL, et le serveur Facebook est capable de savoir quelle requête exécuter. Aucun surcoût, un trafic réseau considérablement réduit et des applications mobiles beaucoup plus rapides.

Relay Modern adopte une approche similaire. Le compilateur Relay extrait les fragments GraphQL colocalisés d’une application, construit les requêtes nécessaires, les enregistre à l’avance sur le serveur et génère les artefacts que l’environnement d’exécution Relay utilise pour récupérer ces requêtes et traiter leurs résultats au moment de l’exécution.

Optimisation anticipée

Le compilateur Relay utilise sa connaissance statique de la structure de la requête pour optimiser ces deux sorties, ce qui contribue à améliorer la performance de l’application. L’optimisation de la requête enregistrée sur le serveur signifie que le serveur peut l’exécuter et renvoyer un résultat plus rapidement. De même, les artefacts optimisés peuvent permettre au logiciel responsable de l’exécution des programmes informatiques de traiter ces résultats plus rapidement.

Des exemples de telles optimisations incluent l’aplatissement, qui réduit les champs dupliqués pour éviter le traitement redondant des données lors de l’exécution, et l’optimisation d’un compilateur (inlining) constant, dans lequel la connaissance statique des conditions peut permettre d’élaguer certaines branches d’une requête au moment de la compilation.

Combinées, ces optimisations permettent de réduire à la fois le temps passé à extraire et à télécharger les résultats de la requête, ainsi que le temps passé à les traiter.

Nouvelles fonctionnalités

En plus de ces améliorations de performance, Relay Modern améliore les fonctionnalités existantes et en ajoute de nouvelles.

Mutations plus simples

Avant même de faire des requêtes statiquement connues, nous savions que nous voulions simplifier le fonctionnement des mutations dans Relay. Les améliorations pour simplifier l’API de mutations sont une demande commune que nous entendons de la communauté.

Le relais classique a quelques caractéristiques puissantes quand il s’agit de mutations, si vous comprenez ce qui se passe sous le capot. L’API des mutations de Relay permet aux développeurs de spécifier une « grosse requête » – toutes les choses qui pourraient changer en réponse à l’application d’une mutation. Lorsqu’une mutation est exécutée, Relay classique croise la grosse requête avec les requêtes qui ont été réellement exécutées pour déterminer l’ensemble minimal de données à restaurer. Cette approche était pratique dans certains cas, mais elle pouvait aussi être imprévisible et confuse. Il était parfois difficile de définir quelle mutation serait générée. Une autre difficulté est celle des requêtes dynamiques d’exécution requises, limitant la possibilité d’effectuer des optimisations de requêtes statiques.

Relay Modern propose à la place une API de mutation explicite : les développeurs spécifient exactement les champs à récupérer après une mutation et la manière exacte dont le cache doit être mis à jour après la mutation. Cela permet à la fois d’améliorer la performance et de rendre le système plus prévisible. L’API permet également une logique arbitraire pour gérer les mutations, ce qui permet de gérer les cas de bords (tels que le tri d’une liste d’éléments au niveau du client) qui n’étaient pas pris en charge dans l’API classique.

Récupération de place

Être une bonne (application) citoyenne sur les appareils mobiles signifie gérer non seulement l’utilisation du processeur mais aussi la mémoire. En conséquence, Relay Modern est conçu dès le départ pour prendre en charge la récupération de place, c’est-à-dire l’éviction de cache, dans laquelle les données GraphQL qui ne sont plus utilisées par aucune vue peuvent être supprimées du cache. Cela est particulièrement utile pour les applications à plus longue durée de vie qui pourraient autrement entraîner la croissance du cache sans limite.

La récupération de place est activée dans l’environnement d’exécution principal et est également soigneusement intégrée dans l’API publique afin que les développeurs n’aient pas à gérer explicitement l’utilisation de la mémoire cache.

Extensible et réutilisable

Relay Modern est capable de beaucoup plus que de réunir React et GraphQL. Avec cette version, celle qui a été utilisée comme une seule bibliothèque est maintenant trois : le compilateur, l’environnement d’exécution et la couche d’intégration React/Relay. Le compilateur Relay (npm: relay-compiler) est un utilitaire générique permettant d’analyser et d’optimiser GraphQL et de générer des artefacts de code. L’environnement d’exécution du Relay (npm: relay-runtime) est un utilitaire générique indépendant de la bibliothèque de vues pour l’écriture, l’envoi et la mise en cache des données GraphQL. Enfin, React Relay (npm: react-relay) intègre Relay avec React, fournissant l’API du conteneur démontrée ci-dessus.

Cette modularité peut permettre à Relay d’être utilisé avec d’autres bibliothèques de vues à l’avenir ou en tant que bibliothèque autonome. Le compilateur de Relay est également conçu pour être étendu afin d’ajouter plus de fonctionnalités basées sur le système de type de GraphQL ou pour être utilisé pour d’autres outils au-delà du développement des applications. Nous sommes ravis de voir où la communauté GraphQL nous aide à prendre ces nouvelles parties de la suite Relay.

Innover en place

Nous sommes ravis de ce que nous avons construit. Mais puisque nous introduisons des changements majeurs dans le fonctionnement de Relay, il est important de fournir un chemin itératif pour que les applications Relay existantes adoptent Relay Modern et apportent de la valeur en cours de route. Nous avons appris de la philosophie de React d’innover en place qu’il est possible d’introduire des changements significatifs dans les logiciels quand cela est fait de manière itérative et qui ne laisse personne derrière.

Pour ce faire, la dernière version de Relay propose une API de compatibilité. Cela vous permet de commencer à utiliser les nouvelles API du Relay Modern dans le contexte d’une application Relay classique existante. Lors de l’utilisation de l’API de compatibilité, vous ne bénéficierez pas encore des requêtes statiquement connues ou de la plus petite requête, mais bénéficierez d’une API plus simple et plus prévisible. Une fois que tous les composants d’une partie de votre application ont été convertis en API moderne, l’environnement d’exécution du Relay Modern peut également être inclus pour améliorer la performance et réduire le surcoût.

À titre d’exemple, nous avons progressivement converti l’onglet Marketplace de l’application Facebook de Relay en Relay Modern, tandis que l’équipe itérait sur les fonctionnalités en même temps. Lorsque la conversion a été terminée et que nous avons activé l’environnement d’exécution du Relay Modern, le délai d’interaction (TTI) du produit sur Android s’est amélioré en moyenne de 900 ms. À l’avenir, nous poursuivons ce processus d’intégration de Relay Modern à nos applications existantes, et nous espérons partager certains des outils que nous avons conçus pour faciliter ce processus.