3 points par GN⁺ 4 시간 전 | 1 commentaires | Partager sur WhatsApp
  • Linear est un outil de productivité qui gère le suivi des issues avec une base de données dans le navigateur et une synchronisation local-first, ce qui permet aux mises à jour d’apparaître dans l’UI en quelques millisecondes
  • La véritable base de données lue par l’UI se trouve dans IndexedDB ; les changements sont d’abord appliqués en local, puis envoyés de manière asynchrone au serveur, qui redistribue ensuite les deltas via WebSocket
  • Le premier chargement repose sur une stratégie qui réduit l’attente réseau : peu de JavaScript et de CSS transférés, découpage agressif du code, modulepreload, pré-cache du service worker et app shell inline
  • Le moteur de synchronisation hydrate les données IndexedDB dans un pool d’objets MobX, enregistre les modifications dans une file de transactions, puis ne rerend que les cellules nécessaires grâce à un état observable au niveau des champs
  • Cette sensation de rapidité vient d’une conception système qui combine saisie centrée sur le clavier, palette de commandes globale, animations compatibles GPU et transitions très courtes

Une base de données dans le navigateur

  • Les applications web CRUD traditionnelles enchaînent clic utilisateur, requête HTTP, lecture de base de données côté serveur, réception de la réponse puis repaint du navigateur, ce qui provoque pendant des centaines de millisecondes des spinners, des skeletons ou une UI figée
  • Linear place dans IndexedDB du navigateur la véritable base de données lue par l’UI ; les changements sont appliqués d’abord en local puis envoyés de façon asynchrone au serveur, qui diffuse ensuite les deltas aux autres clients via WebSocket
  • Dans les applications web rapides, le principal goulot d’étranglement reste le réseau, et le transfert de données entre client et serveur coûte souvent plusieurs centaines de millisecondes
  • Le principe clé de Linear consiste à rendre les requêtes réseau invisibles pour l’utilisateur et à éliminer autant que possible les états de chargement
// Linear
issue.title = "Faster app launch";
issue.save();
  • issue.title = "Faster app launch" met à jour le stockage de données en mémoire, Linear utilisant dans ce cas des observables MobX
  • issue.save(); place dans une file de transactions, que le moteur de synchronisation traite par lots, les changements à envoyer ensuite au serveur
  • L’UI se rerend de manière synchrone à partir des modifications en mémoire locale, tandis que la synchronisation des données se fait en arrière-plan, ce qui élimine le besoin de spinner
  • Tuomas a déclaré lors d’une conférence en 2024 que le tout premier code qu’il avait écrit chez Linear était le moteur de synchronisation, en précisant qu’il s’agissait d’une approche peu courante dans une startup
  • La plupart des applications n’ont pas besoin de construire leur propre moteur de synchronisation comme Linear, et les mises à jour optimistes de TanStack Query et SWR suffisent souvent à obtenir une sensation de rapidité assez proche
  • Les requêtes optimistes apportent un fort gain en supprimant les spinners inutiles, en mettant l’état à jour immédiatement, en validant en arrière-plan et en permettant un rollback si nécessaire
  • La réactivité de l’UI ne doit pas dépendre de la latence réseau, et la vitesse perçue par l’utilisateur est davantage déterminée par la réactivité de l’interface que par le temps de réponse du serveur
  • Aperçu de la stack de Linear

    • Linear est construit sur une stack simple composée notamment de React, TypeScript, MobX, Postgres et un CDN
    • Le frontend utilise React et react-dom, MobX, TypeScript, Rolldown-Vite et plugin-react-oxc, ProseMirror et y-prosemirror, les primitives Radix UI, Emotion et StyleX, Comlink, idb, graphql-request, Sentry et Inter Variable
    • Le backend repose sur Node.js et TypeScript, PostgreSQL sur Cloud SQL, Redis via Memorystore, turbopuffer, Kubernetes sur GCP et Cloudflare Workers
    • Le client desktop est basé sur Electron, tandis que le mobile a été entièrement réimplémenté séparément en Swift pour iOS et en Kotlin
    • Le site marketing utilise Next.js, styled-components et des sprites SVG inline
    • Linear reste en client-side rendering, montrant qu’avec la bonne architecture et le bon design, le CSR peut aussi sembler instantané
    • Garder l’ensemble de l’application côté client permet d’avoir un modèle mental plus simple et de réduire la complexité liée à la séparation serveur/client, à la disponibilité de window ou au paramétrage des en-têtes de cache

Donner la sensation d’un premier chargement instantané

  • Dans un outil de productivité, le temps nécessaire avant de pouvoir réellement commencer à travailler est un détail crucial
  • Le chargement initial d’une application côté client peut être ralenti par la séquence requête index.html, requêtes JavaScript et CSS, traitement de l’authentification, puis requêtes API pour afficher l’application
  • Flux de bundling de Linear : Parcel, Rollup, Vite, Rolldown

    • La sensation d’instantanéité commence avant le runtime, au build time, et pour accélérer le chargement il est essentiel de réduire la quantité de JavaScript et de CSS envoyée
    • Linear a réécrit son pipeline de build dans l’ordre Parcel → Rollup → Vite → Rolldown, chaque transition visant à réduire la quantité de JavaScript/CSS et à améliorer l’expérience développeur
    • Chiffres d’amélioration selon le blog de Linear
    • 50 % de code transféré en moins
    • 30 % de taille en moins après compression
    • 10 à 30 % d’amélioration du chargement de page avec cache froid
    • 59 % de réduction du Time-to-first-paint de la vue active-issues sur Safari
    • 70 à 80 % de consommation mémoire en moins
    • Une grande partie des gains vient de la combinaison du choix de ne cibler que les navigateurs modernes, d’une meilleure dead-code elimination et d’un découpage de code agressif
    • L’abandon du support legacy apporte de gros bénéfices en supprimant les polyfills, la transpilation ES5 et le fallback nomodule
    • Même après optimisation, Linear envoie encore environ 21 Mo de JavaScript minifié, mais le découpe agressivement en centaines de chunks au niveau des routes qui ne sont chargés qu’au moment nécessaire
    • L’essentiel n’est pas le choix d’un bundler précis, mais la suppression des navigateurs legacy, le passage à l’ESM natif et un découpage de code actif
    • L’accumulation de ces étapes a permis de réduire d’environ moitié le JavaScript du premier chargement de Linear, tandis que le temps de build a baissé non pas d’un simple facteur, mais d’un ordre de grandeur
  • Preload après le chargement initial

    • Quand le JavaScript est découpé en petits chunks, chaque chunk peut importer d’autres chunks, ce qui crée un problème de chargement en cascade
    • Linear fait en sorte que le navigateur voie la liste complète avant l’exécution du JavaScript et démarre les requêtes en parallèle, afin que lorsque le script d’entrée atteint son premier import, les chunks associés soient déjà dans le cache
    • En faisant correspondre modulepreload dans le <head> avec la valeur crossorigin du script d’entrée, le navigateur ne traite pas le preload et l’import comme des ressources distinctes et réutilise le fetch mis en cache
    • La timeline d’un chargement à froid passe d’une cascade séquentielle à un seul lot parallèle ; le travail réseau existe toujours, mais il est effectué d’un coup
    • Ce travail se fait en arrière-plan dès que l’utilisateur arrive pour la première fois sur la page de connexion, puis l’application entière est mise en cache quelques secondes plus tard pour pouvoir être servie instantanément
    Publicité
  • Service worker pour plus de vitesse et les fonctions hors ligne

    • Les chunks au niveau des routes pour les vues que l’utilisateur n’a pas encore visitées sont mis en cache en arrière-plan par le service worker
    • Le service worker dispose d’un precache manifest intégré à la source, et environ 1 200 assets hashés couvrent les chunks de route, les icônes et les polices
    • L’architecture fait que l’application entière entre dans le cache quelques secondes après l’arrivée sur l’écran de connexion
    • Les navigations suivantes contournent totalement le réseau, le service worker répondant directement depuis son propre cache sans passer par le cache HTTP
    • Combiné au moteur de synchronisation local-first et aux données utilisateur déjà stockées dans IndexedDB, Linear reste utilisable hors ligne
    • Lecture des issues, création de nouvelles issues, modification du titre et de la description, changement d’état pris en charge
    • Toutes les opérations sont placées en file d’attente dans un store transactionnel local, puis vidées quand la connexion revient
    • modulepreload sert à récupérer en parallèle ce dont on a besoin maintenant, afin que le navigateur ne soit pas bloqué par une chaîne d’import sérielle
    • Le service worker sert à préparer ce dont on aura besoin ensuite
    • Les étapes de chargement rapide de Linear consistent à supprimer autant de code que possible, le découper en petits morceaux et faire du precache en arrière-plan, l’objectif étant d’accélérer les requêtes réseau ou de les supprimer complètement
  • Composition du bundle vendor

    • Chaque package utilisé par Linear est découpé dans un chunk séparé et mis en cache indépendamment
    • Un vendor.js traditionnel invalide le cache de tout le graphe de dépendances dès qu’une seule dépendance est mise à jour
    • Le découpage en chunks de Linear crée un cache vendor granulaire au lieu d’un unique gros fichier ; lorsqu’une dépendance précise est mise à jour, seul ce chunk est invalidé et le reste reste en cache
  • Chargement de gros fichiers de police

    • Un mauvais chargement des polices peut provoquer du texte temporairement invisible, des décalages de mise en page lors du remplacement par la vraie police, et des fetchs dupliqués à cause d’un décalage avec le preload
    • Linear preload la police Inter Variable dans le <head> et fait un preconnect vers static.linear.app
    <link rel="preload"
          href="https://static.linear.app/fonts/InterVariable.woff2?v=4.1";
          as="font" type="font/woff2" crossorigin="anonymous">
    <link rel="preconnect" href="https://static.linear.app"; crossorigin>
    
    • Une variable font gère tout l’axe de graisse 100 à 900 dans un seul fichier woff2, ce qui supprime les requêtes par graisse
    • font-display: swap affiche immédiatement la pile de secours, puis remplace par Inter une fois le chargement terminé
    • Le crossorigin="anonymous" du tag preload est un réglage clé pour permettre au navigateur de réutiliser la ressource en cache lorsque le CSS référence ensuite la même police
    • Sans crossorigin, le mode CORS du preload et celui de la référence CSS diffèrent, ce qui pousse le navigateur à retélécharger la police
  • App shell inline

    • Linear place dans le <head> suffisamment de CSS inline pour dessiner l’état de chargement et afficher l’app shell sans requête vers une feuille de style externe
    • Du JavaScript inline exécute immédiatement les branchements nécessaires à l’expérience initiale
    • Détection d’Electron et du user agent de Linear pour ajouter la classe electron
    • Si localStorage.ApplicationStore n’existe pas, ajout de la classe logged-out
    • Restauration des shell tokens comme l’arrière-plan de la sidebar, la largeur de la sidebar ou le mode sombre depuis localStorage.splashScreenConfig
    • Si l’utilisateur a configuré l’ouverture des liens dans l’application desktop, la largeur de la sidebar est ajustée à 8px
    • Avant même l’arrivée du premier bundle JavaScript sur le réseau, l’écran de chargement a déjà le bon thème, la bonne taille et le bon positionnement selon que l’utilisateur est connecté ou non
    • La manière la plus rapide de donner l’impression que l’application est prête dès que l’utilisateur appuie sur Entrée après avoir saisi l’URL consiste à envoyer l’app shell dans la réponse initiale index.html
    Publicité
  • D’abord le rendu, ensuite l’authentification

    • Un flux d’authentification classique suit l’ordre fetch du HTML, chargement du bundle, validation de session, fetch de l’utilisateur, fetch du workspace, puis rendu, ce qui peut prendre 1 à 3 secondes avant que l’utilisateur voie quoi que ce soit
    • Linear traite l’authentification comme le traitement des changements : il suppose le chemin nominal et vérifie en arrière-plan
    • La plupart des applications CRUD stockent la vraie session dans un cookie HttpOnly, puis ajoutent soit un cookie séparé lisible en JavaScript, soit une requête /me, pour que le frontend sache pendant le démarrage si l’utilisateur est connecté
    • Le script de boot inline de Linear ne vérifie que la présence de localStorage.ApplicationStore au lieu d’un signal d’authentification parallèle
    if (localStorage.getItem("ApplicationStore") === null) {
      document.documentElement.classList.add("logged-out");
    }
    
    • Si ApplicationStore existe, cela signifie que l’utilisateur a déjà utilisé Linear dans ce navigateur et que les données du workspace sont déjà présentes dans IndexedDB
    • Si la valeur est absente, il n’y a aucune donnée à afficher, donc le shell bascule vers une mise en page logged-out et le flux de connexion continue
    • Le vrai jeton de session reste dans les cookies, et le bundle ne préjuge pas l’état de session
    • Si le handshake WebSocket, un delta de sync ou un appel HTTP reçoit un 401 à cause d’une session expirée, le client redirige vers la connexion
    • Le schéma global consiste à faire confiance aux données locales pour rendre immédiatement, à laisser le serveur comme source de vérité pour l’exactitude, puis à réconcilier les deux de manière asynchrone

Moteur de synchronisation

  • La rapidité de Linear part d’une décision fondamentale : considérer le serveur non comme la source of truth de l’interface, mais comme une cible de synchronisation
  • La vitesse ne vient pas d’un seul élément, mais du croisement de trois axes
  • 1. Les données sont déjà là

    • Au démarrage de l’app, l’espace de travail n’est pas récupéré depuis le serveur ; il est réhydraté depuis IndexedDB vers un pool d’objets MobX en mémoire
    • Toutes les requêtes de l’UI pointent d’abord vers le pool d’objets, et comme les issues sont déjà sur l’appareil de l’utilisateur, il n’y a pas d’état « loading issues »
    • En montant en charge, Linear a découpé les données du moteur de synchronisation en chunks selon un principe proche de celui des bundles JavaScript
    • Les deux tables les plus lourdes, Issue et Comment, ne sont pas récupérées d’un coup mais lazy-hydrated au moment nécessaire
    • Cette approche correspond à un découpage du code au niveau des données, ce qui fait dépendre le coût de démarrage non pas de la taille de l’espace de travail, mais de sa structure
    • Un espace de travail avec 10 000 issues démarre presque aussi vite qu’un espace avec 100 issues
    • Quand on entre dans un projet, les issues sont déjà là ; quand on filtre par assignee, les index sont déjà construits
  • 2. Les changements n’attendent pas le réseau

    • Quand l’état d’une issue change, trois choses se produisent presque en même temps
    • La mise à jour d’un observable MobX répercute le changement dans l’UI
    • Le changement est enregistré dans la file transactionnelle durable d’IndexedDB
    • Le changement est ajouté à la file d’envoi vers le serveur
    • À ce stade, le réseau n’a pas encore été utilisé
    • L’utilisateur n’attend pas pour voir sa propre modification ; les retries, rollbacks et reload across durability sont tous gérés en arrière-plan
    • Si le serveur refuse, l’observable revient à son état précédent et un bref flicker se produit, mais la plupart des changements invalides sont interceptés avant même la création de la transaction
    • Le flux de Linear part d’un changement local et traite le serveur non comme une étape d’autorisation, mais comme une étape de confirmation
  • 3. Un delta, une cellule

    • Quand le serveur confirme une modification de l’utilisateur ou de quelqu’un d’autre, une petite enveloppe JSON indiquant ce qui a bougé est renvoyée au client
    • Le client applique ensuite la modification en écrivant la valeur dans l’observable MobX concerné
    • Toutes les propriétés de modèle dans Linear sont chacune observables, et tous les composants qui lisent ces propriétés sont enveloppés dans observer()
    • MobX peut ainsi savoir précisément quel composant dépend de quel champ
    • La modification d’un seul champ dans une issue ne relance le rendu que des composants qui lisent ce champ, sans rerendre toute la liste parente ni toute la barre latérale
    • La mise à jour de 50 issues entraîne le rerendu de 50 cellules, pas de toute la liste
    • Même dans un espace de travail très actif où 10 personnes éditent en même temps, le coût de réception des mises à jour augmente en fonction des éléments réellement modifiés, pas de tous les éléments visibles à l’écran
    Publicité
  • Pourquoi ces trois éléments fonctionnent ensemble

    • Avec seulement une base de données locale et sans écriture optimiste, on a toujours un spinner au moment de sauvegarder
    • Avec seulement l’écriture optimiste et sans observables granulaires, chaque mise à jour provoque encore des saccades
    • Avec seulement des observables granulaires et sans base de données locale, l’attente subsiste toujours au chargement initial
    • La rapidité de Linear n’est pas la propriété d’une seule couche, mais du système dans son ensemble
    • Le bundler et le shell de chargement rendent le premier paint rapide, et le moteur de synchronisation maintient cette sensation de rapidité après le début de l’utilisation

Un design pensé pour la vitesse

  • La vitesse est à la fois un problème d’ingénierie et de design
  • Si le chemin d’action le plus rapide exige une souris, trois menus et des clics, l’utilisateur paie ce coût d’étapes quelle que soit la vitesse du moteur sous-jacent
  • Un autre axe de la vitesse de Linear est l’intégration du clavier comme outil principal de navigation et d’exécution des tâches
  • Toutes les opérations courantes ont un shortcut, la command palette s’ouvre avec une seule frappe, et le menu clic droit a été développé sur mesure
  • Chaque action a son shortcut

    • Un seul caractère permet d’éditer l’issue focalisée, des combinaisons de deux lettres servent à la navigation, et les modificateurs sont utilisés pour les actions globales
    • Dès les débuts de Linear, les shortcuts faisaient partie des fondations, et le moteur de synchronisation a aussi été conçu pour permettre d’exécuter n’importe quelle action à tout moment
    • Les shortcuts sont visibles un peu partout dans l’UI, et les plus fréquents tiennent en un seul caractère
    • Pour ne pas exclure les débutants, toutes les actions restent aussi accessibles à la souris
  • La command palette est toujours à une frappe

    • ⌘ k ouvre la command palette qui permet de rechercher presque toutes les actions de Linear
    • La recherche couvre les issues, les projets, les labels, les changements d’état, la navigation, la création d’issues, les paramètres, le changement de thème, etc.
    • La command palette interroge le pool local d’objets MobX, et non le serveur, ce qui la rend très rapide
    • Toute l’application est accessible depuis un seul panneau, et la navigation, la création d’issues et les changements d’état passent tous par la recherche
    • La command palette s’adapte au contexte de travail actuel et sert aussi à enseigner les actions clés et les shortcuts propres à chaque vue
    • Une application rapide a besoin à la fois d’une excellente ingénierie et d’un excellent design : la vitesse d’ingénierie accélère chaque interaction, tandis que la vitesse de design raccourcit le chemin jusqu’à l’interaction
    • Dans un outil utilisé toute la journée, la différence entre un shortcut et un parcours à la souris de deux secondes s’accumule sur chaque action

Animations

  • De mauvaises animations peuvent, à la toute fin, gaspiller de nouveau les millisecondes gagnées grâce au chargement initial, aux mises à jour et à l’optimisation des requêtes base de données
  • Un élément comme une animation de height sur 500 ms peut ruiner les efforts faits pour éviter de faire attendre l’utilisateur
  • Il n’y a que quelques propriétés à animer

    • Les changements de propriété dans le navigateur ont trois niveaux de coût selon leur position dans le pipeline de rendu
    • Les propriétés composites transform et opacity délèguent le travail au GPU et s’exécutent indépendamment du main thread
    • Les propriétés qui déclenchent un paint, comme color, background-color, border-color, fill, évitent le layout mais provoquent un redessin des pixels
    • Les propriétés qui déclenchent un layout, comme width, height, top, left, margin, padding, forcent le recalcul de la position de tous les éléments ensuite et ne devraient pas être animées
    Publicité
    /* Approche de Linear */
    .row:hover {
      background-color: var(--color-bg-hover);
      transition: background-color 0.12s;
    }
    .icon-arrow {
      transform: translateX(0);
      transition: transform 0.15s;
    }
    
    • Si l’on anime margin-left, le layout de toutes les lignes sous la ligne survolée est recalculé à chaque frame pendant toute la transition de 200 ms
    • Dans une longue liste d’issues, cette différence sépare une interface fluide d’une interface saccadée
    • La plupart des propriétés animées par Linear sont des propriétés composites comme transform et opacity, avec parfois background-color et border-color
  • Il faut savoir rester sobre

    • Dans un outil utilisé au quotidien, des animations spectaculaires dignes d’un site marketing peuvent gêner le travail
    • Même un léger délai au survol, mal placé, peut devenir perceptible pour l’utilisateur
    • Beaucoup d’animations de Linear sont efficaces parce qu’elles se réfèrent à leur point d’origine
    • Le popover de statut s’agrandit depuis la pastille de statut, et le panneau agent glisse depuis le bouton de bascule
    • Ce mouvement n’est pas un simple fade décoratif : il joue un rôle spatial en indiquant d’où vient le nouvel élément
  • Garder des durées courtes et immédiates

    --speed-highlightFadeIn: 0s;
    --speed-highlightFadeOut: .15s;
    --speed-quickTransition: .1s;
    --speed-regularTransition: .25s;
    --speed-slowTransition: .35s;
    
    • Beaucoup de design systems définissent des durées par défaut plus longues que nécessaire
    • La durée standard de Material est de 200 ms, et le spring d’iOS se rapproche de 350 ms
    • Les valeurs par défaut de Linear sont plus courtes que les pratiques habituelles du secteur
    • Linear utilise un timing asymétrique entre l’entrée et la sortie
    • Le surlignage au survol, les popovers et le panneau agent apparaissent immédiatement à l’ouverture, puis disparaissent en fade out sur 150 ms à la fermeture
    • La fenêtre agent apparaît immédiatement puis s’estompe, à la manière de macOS

Comment Linear est rapide

  • Les performances de Linear ne reposent ni sur un secret unique ni sur une seule technologie, mais sur l’accumulation de centaines de bonnes décisions
  • Une grande partie de cette approche est simple : fixer tôt une architecture adaptée aux utilisateurs, puis s’y tenir, sans Next, TanStack ni framework tape-à-l’œil
  • Le serveur n’agit pas comme source of truth de l’UI, mais comme cible de synchronisation
  • La base de données est dans le navigateur, et les modifications sont d’abord appliquées localement avant d’être réconciliées en arrière-plan
  • Le chargement initial envoie moins de code, mais en davantage de morceaux, et le service worker met le reste en precache pendant que l’utilisateur est sur la page de connexion
  • L’authentification suppose le chemin nominal à partir de l’état local, puis vérifie ensuite
  • Le moteur de synchronisation hydrate depuis IndexedDB vers des observables MobX par propriété, de sorte qu’une mise à jour de 50 issues se traduit par le rerender de 50 cellules, et non par le rerender de toute la liste
  • Le modèle d’entrée est keyboard-first, avec des raccourcis et une command palette globale pour toutes les actions courantes
  • Les animations restent sur des propriétés compatibles GPU, et les propriétés qui déclenchent un layout ne sont pas animées
  • La partie difficile n’est pas tant l’implémentation elle-même que la discipline consistant à rester focalisé pendant des années sur la qualité des détails, alors que le codebase mûrit, s’étend et rencontre de nouvelles contraintes

1 commentaires

 
GN⁺ 4 시간 전
Commentaires sur Hacker News
  • Si vous voulez intégrer ce type d’expérience dans votre application, Zero(https://zero.rocicorp.dev/) vaut le détour
    Démo live : https://gigabugs.rocicorp.dev/
    Il y a aussi une liste d’alternatives ici : https://zero.rocicorp.dev/docs/when-to-use#alternatives
    Si le fonctionnement interne vous intéresse, la documentation de conception de Replicache mérite aussi le détour : https://doc.replicache.dev/concepts/how-it-works
    Replicache est le prédécesseur de Zero, et le protocole central fonctionne toujours de la même manière

    • Je recommande vraiment Zero. Les abstractions sont excellentes, et c’est un logiciel conçu avec beaucoup de soin
      Au-delà du gain de performance évident apporté par la synchronisation des données côté client, j’ai aussi été surpris par la simplicité que cela apporte au code React. Avec un moteur de synchronisation, la majeure partie de l’état côté client disparaît, et on peut raisonner sur l’essentiel du code des composants de manière synchrone
    • J’utilise Zero depuis quelque temps. Au départ, on voulait construire en interne un moteur de synchronisation à la Linear, puis on a découvert Zero
      Sans monter une équipe dédiée, c’est probablement ce qui s’en rapproche le plus
    • En tant qu’utilisateur de Zero, c’est exactement l’outil qu’il faut quand on veut une expérience utilisateur où l’interface se met à jour quasi instantanément, en quelques millisecondes, dès que la base de données change
  • J’ai toujours entendu dire que Linear était rapide, mais après l’avoir utilisé tous les jours, mon enthousiasme est retombé. La recherche est assez lente, l’interface manque souvent de réactivité et, même si c’est joli, « Pulse » ressemble à un déluge de bruit même à petite échelle
    Il est difficile de retrouver ce dont on a besoin, au point que je finis par tout mettre en favoris. Le Trello des débuts restait de très loin la meilleure expérience de suivi de projet

    • Je déteste vraiment ces moments, en réunion ou pendant un huddle, où j’essaie d’ouvrir un ticket et où tout le monde reste à regarder maladroitement pendant une attente absurdement longue, qu’il s’agisse d’un chargement ou d’un problème de cache
  • L’an dernier, quelqu’un a fait de la rétro-ingénierie du moteur de synchronisation de Linear, l’a publié sur GitHub et y a ajouté une excellente explication
    https://github.com/wzhudev/reverse-linear-sync-engine/blob/m...

  • Ces applications web de synchronisation local-first sont vraiment intéressantes et peuvent être très utiles, mais la prémisse me semble quelque peu erronée
    La prémisse est du type : « Dans Linear, quelques millisecondes suffisent pour mettre à jour une issue. Une application CRUD traditionnelle met environ 300 ms pour faire la même chose », ou encore « Toute donnée qui transite entre le client et le serveur coûte des centaines de millisecondes »
    On ne peut certes pas résoudre le problème fondamental de l’augmentation du temps d’aller-retour entre client HTTP et serveur à cause de la vitesse de la lumière, mais on peut placer le backend près des utilisateurs et l’optimiser pour qu’il soit rapide
    Par exemple, il est tout à fait possible d’exploiter le backend d’une application web avec un aller-retour d’environ 10 ms pour la plupart des utilisateurs, et de faire en sorte que le backend rende une réponse lui aussi en une dizaine de millisecondes. Autrement dit, même une application CRUD traditionnelle peut ramener la même opération non pas à 300 ms, mais à environ 30 ms

    • Je trouvais déjà étrange qu’on présente 300 ms comme rapide, car, de mémoire, un TTFB de 30 ms a longtemps été l’objectif
      Il est possible que, pour des raisons valables, le backend de Linear prenne plus de temps et ait besoin de l’aide du frontend, mais on ne peut pas généraliser cela. Chaque morceau de JavaScript a aussi son propre coût
    • Dire qu’« il est possible d’exploiter le backend d’une application web avec un aller-retour d’environ 10 ms pour la plupart des utilisateurs » est étrange. La seule région AWS réellement sous les 10 ms depuis us-east-1 est pratiquement us-east-2 : https://www.cloudping.co/
      us-west-1 est à 60 ms, eu-centra-1 à 100 ms, et l’Asie à 200 ms. Et cela concerne le trafic entre data centers ; la latence réelle sur l’internet public jusqu’aux connexions domestiques est bien pire
      La base de données doit se trouver dans une seule région précise. Où qu’on la place, la majorité des utilisateurs sur Terre sera à plus de 100 ms
      Peu importe où se trouve l’endpoint, car pour lire et écrire les données, cet endpoint doit communiquer avec la base de données. Dès qu’on cherche à répliquer les données au plus près des utilisateurs, on finit de toute façon par posséder une base de données de synchronisation local-first
      Que vous la construisiez vous-même ou utilisiez une solution du commerce, cette base de données répliquée hérite de tous les mêmes problèmes que la synchronisation côté client, et il reste encore une latence réseau importante. On ne contourne pas les lois de la physique : pour la plupart des utilisateurs, il faut soit accepter un commit à 0,25 seconde, soit choisir la cohérence éventuelle, autrement dit la synchronisation
    • Ce n’est possible que si les utilisateurs sont relativement proches les uns des autres ou, comme c’est tristement fréquent, si seule la rapidité pour les utilisateurs américains compte et que le reste du monde importe peu
      Bien sûr, on peut déployer un « backend intermédiaire » sur quelque chose comme un réseau edge CDN mondial, mais à ce moment-là on paie la même complexité que dans cette approche qui consiste à mettre ce « backend intermédiaire » sur le client
    • On peut mettre le backend intermédiaire sur le client, écrire les changements non encore traités dans un stockage local, puis laisser un worker en arrière-plan les envoyer au backend avec les retries nécessaires
      Dans le pire des cas, le worker en arrière-plan émet un message d’échec de mise à jour, que le thread UI reçoit et affiche. Le chemin de réussite, lui, reste fulgurant
    • Est-ce encore faisable lorsqu’il existe une base de données que tous ces backends edge doivent partager ?
  • Écrire une base de données à cohérence éventuelle est difficile, et ça peut convenir au cas d’usage de Linear, mais ne pas savoir si ma mise à jour est bien arrivée au serveur, donc à l’équipe, pose problème
    Dans d’autres projets auxquels j’ai participé par le passé, les délais de synchronisation ont causé d’innombrables problèmes, donc je choisis toujours une solution synchrone. Je ne sors les fonctionnalités tape-à-l’œil qu’en cas de nécessité absolue, et je préfère optimiser le serveur à fond puis laisser l’utilisateur absorber la latence réseau

    • J’ai déjà eu des incohérences avec Linear, mais Jira est une décharge, donc bon, on n’y peut pas grand-chose
  • On utilise Linear dans ma boîte. Je sais que je suis minoritaire, mais l’expérience utilisateur est vraiment pénible. J’ai aussi du mal à appeler ça rapide
    La page elle-même se charge techniquement à une vitesse correcte, mais la moitié du temps on voit des chiffres changer sur la page sans aucun indicateur visuel montrant que le chargement des données est encore en cours

    • Un problème fréquent avec Linear, c’est que des écritures répétées s’écrasent parfois l’une l’autre. On tape, on s’arrête un instant pour réfléchir, on retape, et Linear remet les données dans l’état de la première saisie
      C’est au point qu’avec Linear, je crée juste l’issue avec une description d’une phrase, puis je vais sur GitHub pour remplir les détails. C’est à peu près la seule chose que Linear fait bien et vite
    • Linear est devenu exactement ce qu’il voulait éliminer, à savoir un outil complexe
      C’est regrettable, mais pour qu’une entreprise survive et monte en gamme, il n’y a pratiquement pas d’autre voie
    • Je n’utilise pas Chrome, donc je ne sais pas si ça joue, mais les pages Linear se figent souvent ou mettent plusieurs secondes à se charger la première fois
      Je n’emploierais pas le mot « rapide ». Quand le chargement prend déjà 30 secondes, réduire la mise à jour d’une issue de 300 ms à « quelques » millisecondes n’a plus beaucoup d’importance
    • Dans mon ancien boulot, on est passés de Linear à Jira à cause d’une UX bizarre. Il y avait beaucoup d’icônes difficiles à interpréter, une faible découvrabilité, et presque aucune indication expliquant pourquoi le contenu de la page changeait
    • C’est vraiment affreux. J’ai dû demander à un collègue comment ajouter une date d’échéance à un élément, parce que c’était caché dans le panneau de navigation
      C’est mieux que Jira, mais la barre est très basse
  • Très cool. Je me dis que je pourrais intégrer quelque chose de similaire dans le jeu navigateur et le moteur que je développe, pour peut-être éliminer complètement les états de chargement après le premier chargement. Le mien repose sur une architecture d’assets statiques 100 % côté client, sans serveur
    Je suis obsédé par les performances de ce jeu. Avant le week-end dernier, j’avais du mal à maintenir 120 fps sur un M1 MacBook Pro en simulant 128 joueurs simultanés, avec pathfinding, logique stratégique lourde et rendu, le tout dans le viewport, avec de très rares chutes de frame et un temps de frame d’environ 4 ms
    J’ai fait un gros travail d’optimisation pendant le week-end, et maintenant je peux simuler 2048 joueurs simultanés avec un temps de frame inférieur à la milliseconde. Ce chiffre inclut le rendu, toute la logique et même la génération procédurale
    J’ai aussi appliqué un throttling CPU de 11,2x pour simuler des appareils mobiles bas de gamme, et même là j’obtiens un 60 fps stable avec un temps de frame d’environ 5 ms pour 256 à 512 joueurs simultanés. Mon principal goulot d’étranglement en ce moment, c’est un petit problème de logique et le temps de démarrage/boot à améliorer sur les appareils peu puissants, et je pense qu’il y a des choses à apprendre de Linear

  • J’ai toujours trouvé que Linear était en fait assez lent. À une époque, il m’est arrivé de laisser un onglet ouvert et de voir le CPU monter à 100 % pendant des semaines

    • Sous Firefox, ça semble aussi consommer beaucoup de mémoire. Impossible de garder plus de quelques onglets Linear ouverts en même temps
  • C’est intéressant, mais pour être honnête je n’ai jamais trouvé Linear « rapide ». Comme la plupart des apps web, il y a de la latence, même si comparé à JIRA, c’est évidemment l’éclair
    Linear lui-même est excellent, et après la torture JIRA, c’est vraiment rafraîchissant. Mais si on veut parler de routage optimiste et de « rapidité », ne faudrait-il pas commencer par Gmail ?

  • La réponse à la vitesse, c’est le préchargement. En gros, on télécharge une base de données cliente au moment de l’initialisation et on met en place une stratégie d’invalidation du cache
    J’ai créé starfx pour gérer l’aspect synchronisation des données dans ce paradigme : https://starfx.bower.sh/learn#data-loading-strategy-stale-wh...