Ouverts à de nouveaux partenariats produit

L’offline-first est une décision de modèle de données, pas une fonction de cache

La plupart des applications ajoutent le support hors ligne à la fin et découvrent que c’était depuis le début une question d’architecture. Ce qui doit réellement changer, et les règles de conflit entre lesquelles il faut choisir.

« Faites que ça marche hors ligne » sonne comme une fonctionnalité que l’on ajoute en un sprint. Ce n’en est pas une. Cela change où vivent vos données et qui décide de ce qui est vrai, et cela touche tout.

La bonne nouvelle est que ce travail est bien compris. La mauvaise est que l’ajouter après coup coûte plusieurs fois le prix de l’avoir prévu dès le départ.

Deux architectures

L’architecture par défaut est autoritaire côté serveur. L’écran reflète le serveur. Une action envoie une requête, attend, et met à jour à la réponse. Hors ligne signifie cassé, parce qu’il n’y a rien à afficher et nulle part où écrire.

L’alternative est le local-first. L’appareil possède sa propre base de données et l’interface y lit. Les écritures vont immédiatement dans le stockage local et l’écran se met à jour depuis lui. Un processus d’arrière-plan réconcilie avec le serveur quand une connexion existe.

Cette inversion est tout l’enjeu : le réseau cesse d’être entre l’utilisateur et ses données. Tout le reste en découle.

Ce qui change vraiment

Chaque enregistrement a besoin d’une identité générable hors ligne

Si le serveur attribue les identifiants, un appareil ne peut rien créer hors ligne sans inventer un identifiant temporaire puis réécrire ensuite toutes les références. Générez les identifiants côté client, avec des UUID ou équivalents, dès le premier jour. Cette décision est bon marché en amont et douloureuse à rattraper.

Les écritures deviennent une file, pas un appel

Une action ajoute une intention à une file locale durable et retourne immédiatement. Un processus de synchronisation la vide. Ce qui signifie que la file doit survivre à la fermeture de l’application, réessayer avec un délai croissant, et être idempotente, car vous enverrez certaines opérations deux fois.

Les suppressions cessent d’être des suppressions

Si un appareil supprime une ligne hors ligne et que la synchronisation n’envoie que ce qui existe, le serveur n’en saura jamais rien et vous la renverra aimablement. Il vous faut des pierres tombales : la suppression consignée comme un fait horodaté, conservée assez longtemps pour que tous les appareils la voient.

L’interface doit montrer l’état de synchronisation

Dès lors qu’une écriture peut réussir localement puis échouer plus tard, l’utilisateur doit le savoir. Pas un indicateur de chargement partout, mais une information honnête : enregistré localement, synchronisation en cours, synchronisé, échec. Le cacher produit le pire résultat possible : quelqu’un qui croit son travail à l’abri alors qu’il ne l’est pas.

Choisir une règle de conflit

Deux appareils modifient le même enregistrement hors ligne. Les deux reviennent. Quelque chose doit trancher. Il n’y a pas de réponse universellement juste ; il y a une réponse juste pour vos données.

StratégieFonctionnementAdaptée à
La dernière écriture gagneL’horodatage le plus élevé écraseRéglages, état par appareil, champs à faible enjeu
Fusion par champHorodatage par champ ; fusionne ceux qui ne s’opposent pasEnregistrements où chacun modifie des parties différentes
Journal d’opérationsStocke les intentions et les rejoue dans l’ordreCompteurs, listes, tout ce qui est additif
CRDTStructures qui fusionnent de façon déterministeTexte collaboratif et documents partagés
Demander à l’utilisateurPrésenter les deux et laisser choisirUniquement les conflits rares et à forte valeur

Deux avertissements tirés de l’expérience. « La dernière écriture gagne » avec les horloges des appareils perd des données en silence, car ces horloges sont fausses ; utilisez des horodatages attribués par le serveur ou des horloges logiques. Et « demander à l’utilisateur » n’est pas un défaut ; c’est la sortie de secours pour les cas trop coûteux à résoudre automatiquement. Interrogés assez souvent, les gens cliquent sans lire.

Choisissez la règle de conflit par type de donnée, pas par application. La progression des cartes d’un utilisateur peut être additive et fusionner proprement. Son nom affiché peut relever de « la dernière écriture gagne ». La même règle pour les deux est fausse pour l’une des deux.

La synchronisation elle-même

La forme qui fonctionne pour la plupart des produits :

  1. Chaque enregistrement porte une version ou un horodatage de mise à jour attribué par le serveur.
  2. Le client stocke un curseur : le point jusqu’auquel il est à jour.
  3. À la synchronisation, le client envoie les écritures en file et son curseur.
  4. Le serveur applique les écritures, résout les conflits, et renvoie tout ce qui a changé depuis ce curseur ainsi qu’un nouveau curseur.
  5. Le client applique les changements et avance le curseur de façon atomique.

L’atomicité de l’étape 5 compte plus qu’il n’y paraît. Si vous avancez le curseur puis échouez à appliquer, vous avez sauté des changements en silence et rien ne viendra vous le dire.

Testez délibérément les cas laids

Les bogues hors ligne n’apparaissent pas dans les tests ordinaires, parce que les tests ordinaires ont du Wi-Fi. Intégrez ceci à votre plan de test :

  • Mode avion pendant une semaine, puis reconnexion avec des centaines d’opérations en attente.
  • Arrêt forcé en pleine synchronisation. Tuez l’application pendant qu’un lot est en vol. Rien ne doit être perdu ni appliqué deux fois.
  • Portails captifs. Le pire état réseau : l’appareil annonce une connexion et toutes les requêtes renvoient une page de connexion. Elles doivent échouer proprement, sans corrompre l’état.
  • Décalage d’horloge. Réglez l’horloge de l’appareil un jour à côté et vérifiez que l’ordre tient.
  • Deux appareils, même compte, tous deux hors ligne, modifiant les mêmes enregistrements. C’est là que les règles de conflit gagnent leur salaire.

Quand ne pas le faire

L’offline-first représente un vrai travail et n’est pas toujours justifié. Passez votre tour quand l’application n’a de toute façon aucun sens sans réseau, comme une messagerie en direct ou des paiements ; quand les données sont intrinsèquement partagées et simultanées, où vous voulez sans doute du temps réel plutôt que du hors ligne ; ou quand il s’agit d’un outil interne peu utilisé où le coût dépasse le bénéfice.

Faites-le quand les gens utilisent l’application là où la connectivité est irrégulière : trajets, transports, avions, zones rurales, bâtiments mal couverts. Pour une application d’apprentissage, cela représente la majorité des sessions, c’est pourquoi nous le traitons comme une hypothèse de départ et non comme une demande de fonctionnalité.

La version la moins chère

Si une architecture local-first complète est trop lourde pour votre étape actuelle, il existe un large terrain intermédiaire qui vaut la peine :

  • Mettez les lectures en cache agressivement pour que l’application s’ouvre sur du contenu et non sur un indicateur de chargement.
  • Mettez les écritures en file de façon durable même sans synchronisation complète, pour qu’une action ne soit jamais perdue à cause d’une coupure.
  • Générez les identifiants côté client dès maintenant, car cela ne coûte rien aujourd’hui et débloque tout plus tard.

Ces trois points apportent l’essentiel du bénéfice perçu pour une fraction du travail, et aucun n’est perdu si vous allez plus loin ensuite.

Mobile Ingénierie Architecture