Accueil / Articles

Concevoir des interfaces utilisateur optimisées pour le mode hors ligne en tant que répliques : files d’attente, tentatives répétées, conflits

Découvrez pourquoi l’approche « offline-first » transforme le navigateur en une réplique de données, et comment les stockages locaux, les files d’attente de mutations durables, les tentatives idempotentes et les règles de conflit s’articulent entre eux.

Éditeur : REACTAPP.TOP12 min de lecture2736 mots

Sources: The Architecture Behind an Offline-First Frontend

Enregistrer. L’architecture conventionnelle des interfaces utilisateur traite tous ces cas comme des exceptions : appeler l’API, attendre, mettre à jour l’état, continuer.

La conception « offline-first » part d’un principe inverse : le réseau peut tomber en panne, mais l’application doit rester utile malgré tout. Cela ressemble à une amélioration de l’expérience utilisateur, mais il s’agit en réalité d’un changement structurel. Dès que le navigateur peut lire des données, les modifier, les conserver et les synchroniser ultérieurement, il cesse d’être un client léger pour devenir un nœud d’un système distribué. Ce guide explique en détail ce que cela signifie dans la pratique : où les données doivent être stockées, comment les écritures sont mises en file d’attente et tentées à nouveau de manière sécurisée, comment l’interface utilisateur doit communiquer l’état de synchronisation, et pourquoi la gestion des conflits relève finalement d’une décision stratégique liée au produit.

Éloigner le réseau du chemin critique

Imaginons une application de notes simple. Quelqu’un ouvre une note, modifie son titre et la sauvegarde. Dans une conception traditionnelle, le flux se déroule comme suit :

User → React → Server
              ↓
            Data

Le serveur est intégré directement dans l’interaction. Un serveur lent entraîne une interface utilisateur lente ; une connexion interrompue empêche les enregistrements ; une demande échouée oblige l’application à décider sur le champ quoi faire des modifications de l’utilisateur.

Une conception axée sur le mode hors ligne transfère une part importante de cette responsabilité vers le navigateur :

User → React → Local Store
                    ↓
                Sync Later

Le serveur n’en devient pas moins important. Pour la plupart des systèmes, c’est toujours là que réside la vérité partagée. Ce qui change, c’est qu’il n’a plus besoin d’être accessible pour chaque interaction. Cette séparation, entre la reconnaissance de l’action de l’utilisateur et sa transmission au backend, constitue le fondement sur lequel repose tout le reste.

Le navigateur gère désormais des états importants

Si l’application doit continuer de fonctionner en l’absence de connexion, elle a besoin d’un stockage fiable sur le dispositif. Pour tout ce qui dépasse de simples préférences, cela signifie généralement l’utilisation d’IndexedDB ou d’une base de données de niveau supérieur qui s’y superpose. La bibliothèque spécifique importe moins que les conséquences : le navigateur gère désormais des données dont la perte serait inacceptable.

Pensez aux enregistrements en cache et aux brouillons non envoyés, aux modifications prêtes à être transmises, aux métadonnées de synchronisation telles que les horodatages et les numéros de version, ainsi qu’aux registres indiquant ce qui a déjà été téléchargé. Cela soulève une question à laquelle une application classique basée sur des requêtes et des réponses n’a jamais à répondre : que se passe-t-il lorsque la copie du navigateur et celle du serveur diffèrent ? Dans une application conçue pour fonctionner en mode hors ligne en priorité, ce n’est pas un cas particulier ; c’est la norme.

Le cache des lectures n’équivaut pas à une approche en mode hors ligne en priorité

Cette distinction est facile à confondre. Une application peut mettre en cache les réponses API tout en dépendant pleinement du réseau. Supposons qu’elle récupère une liste de projets et en stocke le résultat localement, de sorte que la liste réapparaisse sans connexion. C’est utile, mais cela ne couvre que les lectures.

La conception devient véritablement optimisée pour le mode hors ligne lorsque les utilisateurs peuvent modifier des données en étant déconnectés : créer un projet, modifier un document, cocher une tâche, ajuster une configuration. Ces actions ne peuvent pas simplement disparaître parce qu’aucune requête HTTP n’a été envoyée. L’application a besoin d’un endroit pour enregistrer ce que l’utilisateur avait l’intention de faire, afin que cette intention survive jusqu’à ce qu’elle puisse être exécutée.

Toute écriture hors ligne devient une obligation de synchronisation

Lorsqu’une personne modifie un document hors ligne, l’approche habituelle consiste d’abord à appliquer la modification dans le stockage local, puis à enregistrer l’opération qui doit encore être envoyée au serveur. En termes simplifiés :

User Saves
    ↓
Save Locally
    ↓
Update UI
    ↓
Add To Queue
    ↓
Sync When Online

Prêtez attention à l’ordre : l’écriture locale et la mise à jour de l’interface ont lieu avant que quoi que ce soit ne soit mis en file d’attente, c’est pourquoi l’interface peut répondre immédiatement. La file d’attente, en fin de processus, mérite plus d’attention qu’elle n’en reçoit généralement. Il s’agit non pas d’un amas de demandes qui ont échoué, mais d’un registre fiable des tâches que l’utilisateur attend que le système termine.

Cela implique des exigences concrètes. La file d’attente doit survivre aux rechargements de page, aux redémarrages du navigateur, aux erreurs temporaires du serveur et aux changements de réseau, ce qui signifie qu’elle doit être stockée de manière persistante plutôt que dans un tableau JavaScript. Elle doit également fonctionner correctement lorsque une opération est exécutée deux fois ou plus. Cette seconde exigence entraîne l’un des modes de défaillance les plus complexes dans tout le système.

Lorsque le succès ressemble exactement à l’échec

Considérez cette séquence : l’utilisateur enregistre les données. La demande arrive. Le serveur écrit les données avec succès. Puis, avant que la réponse ne revienne, la connexion est interrompue. Le client en déduit que l’enregistrement a échoué ; le serveur, lui, sait qu’il a réussi.

Client
  |
  | Save
  v
Server
  |
  | Saved
  X
Response Lost
  |
  v
Client Retries

Si le client réessaie simplement, le serveur pourrait appliquer la même modification deux fois. Une opération « set title » idempotente peut être inoffensive, mais pour des opérations comme « add item », « create order » ou « increment counter », cela entraîne des doublons.

C’est pourquoi l’idempotence est essentielle dans les systèmes offline-first. Chaque mutation nécessite une identité stable afin que le serveur puisse distinguer s’il s’agit d’une tentative de réexécution de la même opération logique ou d’une nouvelle opération. Une approche courante consiste à générer un identifiant d’opération sur le client au moment où la modification est enregistrée pour la première fois, à le stocker avec la mutation mise en file d’attente, et à l’envoyer à chaque tentative. Le serveur suit alors quels identifiants il a déjà traités et, en cas de répétition, il les confirme sans les appliquer à nouveau.

Ce n’est pas une modification liée aux performances. C’est ce qui empêche un réseau peu fiable de transformer des tentatives normales en tâches redoublées. Un détail pratique qu’il convient de bien gérer : l’ID doit être créé une seule fois et conservé avec l’entrée de la file d’attente, et non régénéré à chaque demande, sinon chaque tentative semble être une nouvelle tâche.

L’interface utilisateur optimiste comme expression d’intention

Les mises à jour optimistes sont généralement présentées comme un moyen de rendre les applications plus rapides. Dans une application axée sur le mode hors ligne, leur importance est encore plus grande. Si un utilisateur termine une tâche en mode hors ligne et que l’interface attend une réponse du serveur avant de la refléter, l’application semble simplement défectueuse. Le changement local devrait normalement apparaître immédiatement, et la synchronisation pourra mettre à jour la version distante ultérieurement.

Cela permet une répartition utile des responsabilités :

  • L’état local reflète ce que l’utilisateur a fait.
  • L’état de synchronisation indique si le système partagé a mis à jour ses données.
  • Ces deux éléments sont liés mais pas identiques, et les traiter séparément permet à l’interface d’afficher des informations plus précises qu’un seul indicateur en ligne/déconnecté. Par exemple, un élément peut être marqué comme :

    • sauvegardé sur cet appareil
    • dans la file d’attente pour synchronisation
    • en cours de synchronisation
    • synchronisé
    • nécessitant l’attention de l’utilisateur

    L’utilisateur qui voit « dans la file d’attente pour synchronisation » comprend que le travail est en cours mais pas encore partagé, ce qui est bien plus rassurant qu’un indicateur flou ou un message d’erreur.

    Laissez le stockage local gérer les données, et React les afficher

    Une source courante de complexité accidentelle se présente ainsi : les données sont stockées dans IndexedDB, chargées dans l’état de React, copiées à nouveau dans un stockage côté client distinct, puis tout est synchronisé manuellement. L’application contient alors plusieurs représentations des mêmes enregistrements, ce qui entraîne des valeurs obsolètes, des conditions de concurrence, des re-render inutiles et des bugs de synchronisation qui existent entièrement dans le frontend.

    Un modèle plus propre confie la gestion des données persistantes au stockage local fiable, avec React qui agit comme une vue sur celles-ci :

    Local Store
         ↓
      React UI
         ↓
       User
         ↓
    Local Store
    

    Le boucle est l’élément clé : l’interface utilisateur lit les données depuis le stockage local, l’utilisateur intervient, et cette action écrit à nouveau dans le stockage local, ce qui fait alors fonctionner à nouveau l’interface utilisateur. React s’occupe de gérer les interactions et la présentation ; la base de données est responsable de l’état côté client qui doit persister. En pratique, cela signifie généralement que les composants s’abonnent à des requêtes vers le stockage local plutôt que de copier les résultats dans l’état du composant. Les avantages augmentent avec la taille de l’application, car il n’y a qu’un seul endroit à vérifier lorsque une valeur semble incorrecte.

    Lorsque les deux appareils ont tous deux raison

    La résolution des conflits est le point où l’approche « en mode hors ligne en premier » cesse d’être une question de stockage pour devenir une question liée aux systèmes distribués. Supposons que quelqu’un ouvre le même document sur un ordinateur portable et un téléphone, et que tous deux passent en mode hors ligne. Sur l’ordinateur portable, le titre est changé en Frontend Architecture ; sur le téléphone, en Offline Architecture. Chaque appareil a accepté une modification légitime, sans que l’un ne soit au courant de l’autre. Lorsque tous deux se reconnectent, ils entrent en conflit :

    Laptop              Phone
       |                   |
       v                   v
    "Frontend App"     "Offline App"
       |                   |
       +--------+----------+
                |
                v
             Conflict
    

    Il n’existe pas de solution universellement correcte. La dernière modification doit-elle l’emporter ? La version du serveur doit-elle avoir la priorité ? Les champs peuvent-ils être fusionnés ? Le utilisateur doit-il choisir ? Différents champs doivent-ils suivre des politiques différentes ?

    La réflexion clé est que la résolution des conflits est une décision de produit qui ressemble à une décision technique. Le principe du dernier enregistrement gagne peut être tout à fait acceptable pour, disons, une préférence d’interface utilisateur. Pour les documents collaboratifs, les données financières, les inventaires ou les archives commerciales importantes, il peut être inacceptable, car l’élimination silencieuse d’une des parties entraîne la perte de travail réel ou produit des totaux incorrects. L’architecture doit définir ce que signifie « correct » pour chaque type de données, c’est pourquoi les responsables du produit doivent participer à cette discussion plutôt que de la laisser entre les mains de celui qui écrit le code de synchronisation. Il convient également de noter que le concept de « dernier » est lui-même sujet à caution : les horloges des appareils évoluent, de sorte que les politiques basées sur les timestamps du client peuvent choisir le mauvais gagnant, à moins que l’ordre ne provienne d’une source plus fiable, comme les versions attribuées par le serveur.

    Les modifications à distance nécessitent le même point d’entrée

    Le principe « offline-first » ne signifie pas qu’il faut synchroniser uniquement lorsque l’application s’ouvre. D’autres utilisateurs et d’autres appareils modifient les mêmes données, et le serveur continue de recevoir des modifications en provenance d’autres endroits. Un canal en temps réel ajoute donc une deuxième direction de flux :

    Local Changes
          ↓
         Sync
          ↓
        Server
          ↓
      New Changes
          ↓
     Local Store
          ↓
          UI
    

    Cela conduit à une règle importante : les modifications provenant du serveur doivent aboutir dans le même modèle de données local que celui que l’interface utilisateur lit déjà. Si les modifications locales suivent un chemin et les mises à jour à distance un autre, les composants React doivent gérer une logique complexe pour concilier les deux. En faisant du stockage local le point de rencontre unique, la rendu reste simple : l’interface n’a pas besoin de savoir si une modification provient de cet appareil ou d’un autre.

    Prévoir que le navigateur lui-même peut tomber en panne

    Il existe un autre mode de défaillance facile à négliger : le navigateur peut disparaître avant que le réseau ne soit rétabli. La fenêtre s’ferme, le navigateur plante, le système d’exploitation termine le processus, la batterie de l’ordinateur portable se vide, ou le processus disparaît au milieu d’une synchronisation. Tout travail en attente qui n’existe que dans la mémoire est perdu avec lui.

    C’est pourquoi l’état de synchronisation doit être durable. Une application sérieuse conçue pour fonctionner en mode hors ligne doit considérer le navigateur comme un nœud peu fiable, et sa base de données locale doit contenir suffisamment d’informations pour reconstruire les tâches en attente lorsque l’application redémarre : quelles opérations sont en attente, lesquelles étaient en cours, et quel était l’état dernier connu du serveur. Un test utile consiste à fermer la fenêtre au milieu d’une synchronisation pendant le développement et à s’assurer qu’aucune information n’a été perdue ou dupliquée lors du lancement suivant.

    Les service workers ne font qu’une partie de l’architecture, pas toute l’architecture

    Les workers de service sont très utiles, en particulier pour le stockage en cache des ressources d’une application et l’interception des requêtes réseau ; si vous avez besoin de vous rafraîchir les idées sur leur fonctionnement, consultez comment activer le soutien hors ligne avec des workers de service. Cependant, enregistrer un tel worker ne fait pas d’une application une application priorisant le mode hors ligne :

    • Un worker enregistré ne fait rien pour empêcher la perte des modifications non enregistrées de l’utilisateur.
    • Les fichiers chargés depuis le cache permettent à l’application de démarrer hors ligne, mais ne précisent pas où sont stockées les écritures effectuées en mode hors ligne.
    • Fournir des réponses API obsolètes depuis le cache n’est pas une politique de gestion des conflits.

    Le principe « offline-first » fait partie de l’architecture, et non d’une fonctionnalité du navigateur à activer. Ce qui permet vraiment son fonctionnement, c’est une couche de données locale, des modifications persistantes, un processus de synchronisation, un traitement explicite des échecs, une politique pour gérer les conflits, ainsi qu’une interface utilisateur qui accepte que la synchronisation à distance puisse prendre du temps.

    La complexité se déplace ; elle ne disparaît pas

    La réalité dérangeante est que le logiciel « offline-first » n’est pas plus simple. La complexité est simplement déplacée. Une application conventionnelle peut se contenter d’envoyer une demande et d’attendre la réponse. Une application « offline-first », en revanche, doit faire face à une série de problèmes plus difficiles :

    • Que se passe-t-il si le serveur est inaccessible ?
    • Que se passe-t-il si le navigateur se ferme avant que la synchronisation ne soit terminée ?
    • Que se passe-t-il si la demande arrive mais pas la réponse ?
    • Que se passe-t-il si le même enregistrement est modifié sur un autre appareil ?
    • Que se passe-t-il si les deux modifications sont valides ?
  • Quel résultat est considéré comme correct pour ce produit ?
  • Ce sont des problèmes classiques des systèmes distribués, et le fait que le navigateur fasse partie des participants ne les rend pas plus faciles à résoudre. Avant de choisir une approche priorisant le mode hors ligne, il est utile de se demander si le produit a réellement besoin d’écrire des données hors ligne. Pour un tableau de bord que les utilisateurs ne lisent qu’, des lectures en mémoire tampon accompagnées d’un message clair indiquant le mode hors ligne peuvent offrir la majeure partie de la valeur à moindre coût.

    Les données locales méritent une approche de sécurité au niveau du serveur

    Les données stockées sur un appareil restent des données, avec toutes les responsabilités que cela implique. Si des informations sensibles se trouvent dans le navigateur, il faut envisager ce qui se passerait en cas de compromission de l’appareil, si plusieurs personnes le partagent, ou si un compte se déconnecte et qu’une autre personne s’en sert. La persistance locale doit suivre les mêmes principes de sécurité que le stockage backend :

    • Ne stockez que ce dont le client a réellement besoin.
    • Définissez des règles de péremption ainsi que les actions à effectuer lors de la sortie du compte.
    • N’ayez pas recours au stockage du navigateur comme à un coffre-fort sécurisé.
    • Décidez consciemment quels enregistrements peuvent exister sur le dispositif.

    La stratégie « d’abord hors ligne » améliore la disponibilité, mais cet avantage ne doit jamais se faire au prix de la divulgation d’informations qui n’auraient pas dû être stockées localement en premier lieu.

    Concevoir en tenant compte des échecs plutôt que du scénario idéal

    Une caractéristique frappante de l’approche « d’abord hors ligne » est que l’architecture devient plus claire dès lors que l’on cesse de concevoir pour le scénario idéal. Les questions changent :

    • Au lieu de « comment puis-je récupérer ces données ? », demandez où les données doivent être stockées en l’absence de réseau.
    • Au lieu de « comment puis-je réessayer cette demande ? », demandez comment le client peut savoir si le serveur l’a déjà traitée.
  • Au lieu de se demander comment insérer des données dans l’état React, demandez quelle partie du système est réellement responsable de ces données.
  • Au lieu de considérer des requêtes conflictuelles comme des bugs à corriger, demandez ce que signifie la correction pour ce produit.
  • En répondant de manière cohérente à la deuxième version de chaque question, on obtient une conception plus solide.

    L’objectif est une expérience que les utilisateurs remarquent à peine

    Les utilisateurs ne s’intéressent pas aux moteurs de synchronisation, aux transactions de base de données, aux politiques de retentissement, aux algorithmes de fusion ou à la réplication. Ils veulent continuer à travailler. Ils s’attendent à ce qu’un brouillon reste disponible même après la rupture de connexion, qu’une tâche terminée dans le train le reste également, et à ce qu’une application ne devienne inutile dès que l’icône Wi-Fi change. La mécanique en dessous peut être complexe ; l’expérience utilisateur, elle, ne doit pas l’être.

    Points clés

    L’idée centrale qui unit tout ce design est la suivante : un frontend axé sur le mode hors ligne est un système distribué dans lequel le navigateur héberge une seconde copie des données. Vu sous cet angle, chaque composant prend un rôle plus clair :

    • IndexedDB n’est pas seulement un espace de stockage du navigateur ; c’est également une partie de la couche de données de l’application.
    • La file d’attente des mutations n’est pas un outil d’essai répété ; c’est un enregistrement fiable des intentions de l’utilisateur, et chaque entrée nécessite une identifiant stable pour permettre des essais répétés de manière idempotente.
    • L’interface utilisateur optimiste n’est pas un subterfuge pour accélérer les choses ; elle permet à l’application de rester utilisable pendant que le backend a besoin de se synchroniser.
    • Gestion des conflits n’est pas une situation marginale ; c’est intégré à la façon dont l’application définit sa correction.
    • La synchronisation n’est pas quelque chose à ajouter en fin de processus ; c’est l’architecture même du système.

    Les réseaux tombent en panne, les serveurs ralentissent, les appareils sont temporairement en désaccord et les navigateurs disparaissent sans avertissement. Un frontend conçu pour fonctionner en mode hors ligne assure que le travail de l’utilisateur survive à tout cela, tenant sa promesse même lorsque la connexion est impossible.