On conçoit souvent nos applications React comme agnostiques de l'environnement, car on sort d'une époque où nos applications SPA étaient de simple fichier statiques qu'on déposait sur un FTP, au mieux un CDN, mais en tout avec aucune réactivité. Penser nos applications comme ça empêche de pousser la réactivité au maximum alors qu'on peut le faire très facilement !
Application web ? Environnement ? Kézako ?
Commençons par le commencement et alignons-nous sur ce dont on va parler !
Quand je parle d'application web, je parle de produit avec une partie frontend généralement sous forme de SPA aujourd'hui qui vont attaquer un ou plusieurs backend dans le but de servir des fonctionnalités à un utilisateur sur la durée (au sens, ce n'est pas un utilisateur qui va ouvrir l'URL, regarder quelque chose puis fermer l'onglet, mais plutôt rester entre quelques minutes pour gérer quelques tâches à plusieurs heures pour travailler avec cette application). Je vais me concentrer sur ces applications, car c'est celles qui ont pour moi le plus d'intérêt à être très réactive et évoluer au fil du temps, y compris pendant l'utilisation ! Même si je pense qu'une partie de ce que je vais dire peut s'appliquer à d'autres applications.
Quant à l'environnement, je parle de tout le contexte autour de notre SPA : le/les backend(s), la connexion réseau, les services tiers, la configuration dynamique, etc. Tout ce qui peut ou pourrait avoir de l'influence sur le fonctionnement de notre application.
Une configuration dynamique
Le premier niveau pour avoir une application dynamique c'est de plus compiler la configuration avec l'application (.env
, environment.ts
) mais la charger au démarrage de l'application.
Le plus simple c'est avoir un fichier /config.json
qui chargé avec un fetch au démarrage de l'application.
Je vois parfois aussi le chargement d'un script
/config.js
via une balise script et qui va ensuite ajouter des clés àwindow
. En soi ça fonctionne mais : ça devient moins évident de typer si on fait du TypeScript, on se retrouve avec un script changé et qui potentiellement n'est pas exécuté quand on le pense voir n'exécute pas ce qu'on pense. Un simple fichier json sera forcément géré comme votre application l'attend.
Dans ce json le premier élément qu'on va retrouver c'est l'URL du backend et éventuellement les autres URLs sur lesquels l'application devrait se connecter, par exemple un websocket ou un service externe ou toute infos qui pourraient être nécessaires au bootstrap de notre application.
Comme on fait un appel réseau séparé : pensez à avoir une configuration par défaut pour avoir quelque chose si l'appel échoue ! C'est toujours très frustrant pour les utilisateurs d'avoir seulement une page blanche, et n'avoir aucune information est un point gênant pour vous aussi pour comprendre ce qu'il se passe chez vos utilisateurs !
Des toggles features !
Là on rentre dans une mécanique que je ne vois pas mise en place partout alors que c'est hyper pratique : un moyen d'activer/désactiver des fonctionnalités dynamiquement !
L'idée c'est d'avoir un simple json avec une liste de clé et pour chaque clé un boolean pour activer à true
la fonctionnalité et à false
la désactiver. Ensuite dans notre code on pourra juste faire des if
pour afficher ou non certain éléments, déclencher ou non certains appels réseaux, etc.
Dans tous les cas : une toggle feature frontend n'est pas une sécurité, il faut partir du principe qu'il faut tout revalider côté backend si ça peut avoir un impact négatif !
J'ai déjà vu les toggles fournis au frontend via le fichier /config.json
, ça fonctionne bien si le fichier n'est pas fourni dans le livrable du front pour pouvoir l'éditer sans avoir à subir à nouveau tout le process de mise en production et pouvoir l'éditer très facilement. Dans ce cas c'est très pratique, on a un seul appel pour aller chercher le set de toggle features.
Dans tous les cas, n'hésitez pas à envisager de faire régulièrement des appels à la liste des toggles pour vous assurer que l'état n'a pas changé en cours de route. Dans le cas contraire un utilisateur pourrait voir un bouton qui ne correspond à rien d'activer côté backend pendant des heures tant qu'il ne rafraîchit pas la page, ou tout l'inverse : ne pas voir une nouvelle fonctionnalité tant qu'il n'a pas rafraîchi la page !
À noter qu'il existe une autre manière de gérer ses toggles features de manière complètement dynamique : HATEOAS. Julien Topçu en parle bien mieux que moi ici mais l'idée c'est de faire des APIs orienté métier en faisant porter toute la logique d'enchaînement métier par l'API de sorte que le frontend se concentre uniquement sur l'expérience utilisateur pas sur la technique de ce qu'il est possible d'effectuer. Si on a une API HATEOAS, on peut juste ne pas fournir l'option d'accéder à certains éléments dans nos réponses API et automatiquement l'utilisateur n'y a plus accès (pour peu qu'on gère correctement ça côté front !).
Oups c'est cassé…
Les toggles features c'est bien, mais être complètement dynamique c'est encore mieux ! En effet là on parle d'activer/désactiver à la main des fonctionnalités, mais quid d'un service qui est cassée ? On va continuer de tenter de taper dessus à l'infini alors qu'on sait d'avance que ça ne fonctionnera pas ?
J'aime beaucoup cette citation (extraite d'un talk de Pascal Martin qui jouait son talk "Une application résiliente, dans un monde partiellement dégradé" à Touraine Tech 2024) et je crois que ça résume bien le pourquoi on veut traiter ce genre de cas :
J'ai déjà eu l'occasion de travailler sur une application où à rythme régulier (toutes les 2-3min ou quelque chose du genre) on faisait un appel au BFF (Back For Front) sur une route /health
qui nous renvoyait un json nous indiquant l'état du BFF et de tous les autres services qu'on appelait. Avec cette réponse on activait/désactivait certains pend entier de l'application pour l'utilisateur n'essaie pas d'attaquer le service cassé en lui indiquant que c'était momentanément indisponible de sorte à ne pas créer de la frustration pendant l'usage mais plutôt communiquer avec lui.
Techniquement là on n’est pas forcément obligé de masquer complètement la fonctionnalité, mais plutôt la montrer comme désactiver (si c'est un champ d'édition qu'il ne soit plus éditable, si c'est une remontée de données sur une tuile l'affichage mais un message "momentanément indisponible", etc.). Le but étant de rendre l'application utilisable au maximum en sachant qu'on fournit un service dégradé et que l'utilisateur doit potentiellement le savoir.
Les utilisateurs ne veulent que rarement avoir accès à 100% de l'application à tout instant. Si on monte en volume d'utilisateur à tout instant on aura au moins un utilisateur sur chaque fonctionnalité mais si l'application fonctionne à 80% potentiellement on peut satisfaire une très grande partie des utilisateurs de manière transparente et c'est le plus important ! Car il vaut mieux fournir un service à 80-90% des utilisateurs que aucun parce qu'on a pas géré un cas de service annexe cassé.
Être proactif : régir à la lenteur !
Une application web passe son temps à faire des appels réseaux. Certains prennent beaucoup de temps, certains non, mais on peut rendre ça moins aléatoire ! Pour améliorer l'expérience utilisateur, on peut déterminer le temps moyen qu'est censé prendre une requête : si elle dépasse trop ce temps moyen, on peut agir pour éviter de laisser l'utilisateur attendre sans rien faire, car on sait qu'il se passe quelque chose de bizarre !
Dans pas mal d'application : si le temps d'une requête est beaucoup plus long que d'habitude (3-5 secondes au lieu de 200ms par exemple), soit c'est "pas de chance" (une surcharge temporaire, une saturation dans le réseau, etc.) soit c'est que notre backend n'arrive plus à suivre et ne pourra pas répondre rapidement voir pas du tout.
Dans ces cas-là on peut opter pour plusieurs stratégies :
- couper la requête et renter aussitôt une fois (si on retente plus, on risque de juste surcharger encore plus le backend)
- couper la requête et retenter plusieurs fois en augmentant de manière exponentielle le temps d'attente entre les requêtes (exponential backoff)
- indiquer directement à l'utilisateur que la fonctionnalité n'est pas disponible momentanément
- une combinaison des trois précédentes qui ne sont pas exclusives entre elles
À noter aussi qu'il existe l'API expérimentale Network Information (pour l'instant rejeté par Firefox et Safari pour des questions de vie privée il me semble) qui permet de connaître le type de connexion de l'utilisateur. Avec cette API on peut obtenir le medium de connexion ("bluetooth", "cellular", "ethernet", "wifi", etc.) en appelant navigator.connection.type
, la qualité de la connexion ("slow-2g", "2g", "3g" ou "4g") en appelant navigator.connection.effectiveType
ou encore le round-trip time (temps moyen entre appel et réponse arrondi à 25ms) via navigator.connection.rtt
.
En effet une requête mettra forcément plus de temps si elle est faite en 2g qu'en 3g, en 3g qu'en wifi, en wifi qu'en ethernet. On pourrait imaginer que si cette API devenait plus standard on puisse calculer des timeouts différents en fonction de la qualité de connexion. On pourrait aussi simplement imaginer aussi l'option de désactiver le chargement automatique de certaines parties de l'application si la connexion est trop mauvaise, et avoir des boutons permettant de demander les chargements individuels de ces morceaux de l'application.
Conclusion
Peu importe la solution que vous choisissez : le plus important c'est l'expérience utilisateur, car c'est pour eux l'application existe.
Rendez vos applications les plus réactives possible pour rendre leur expérience la plus simple et efficace possible. Prévoyez la possibilité que tout ce qui n'est pas capital pour une fonctionnalité soit coupé au besoin, que ce soit un service qui donne une information supplémentaire, un service d'IA qui proposer de l'aide ou même de l'analyse d'audience (Matomo, Google Analytics, etc.), si ce n'est pas indispensable, le fait que ce soit en échec ne doit pas bloquer votre utilisateur.
Crédit photo : https://pixabay.com/photos/balance-domino-business-risk-6815204/