- Linear ist ein Produktivitätstool, das Issue-Management-Aufgaben mit einer Datenbank im Browser und lokaler Synchronisierung zuerst verarbeitet, sodass Issue-Updates innerhalb weniger Millisekunden in der UI erscheinen
- Die tatsächliche Datenbank, aus der die UI liest, liegt in IndexedDB; Änderungen werden zuerst lokal angewendet, dann asynchron an den Server gesendet, der Deltas per WebSocket erneut verteilt
- Beim ersten Laden setzt die Anwendung auf eine Ladestrategie mit wenig übertragenem JavaScript und CSS, aggressivem Code-Splitting, modulepreload, Service-Worker-Precache und einer Inline-App-Shell, um Netzwerk-Wartezeiten zu verringern
- Die Synchronisations-Engine hydriert IndexedDB-Daten in einen MobX-Objektpool, speichert Änderungen in einer Transaktions-Queue und rendert durch auf Feldebene beobachtbaren Zustand nur die nötigen Zellen neu
- Die schnell wahrgenommene Geschwindigkeit ist das Ergebnis eines Systemdesigns, das auch tastaturzentrierte Eingabe, eine globale Command Palette, GPU-freundliche Animationen und kurze Übergangszeiten umfasst
Die Datenbank im Browser
- Traditionelle CRUD-Webapps durchlaufen nach einem Klick des Nutzers einen HTTP-Request, eine Datenbankabfrage auf dem Server, den Empfang der Antwort und einen Browser-Repaint, wodurch über Hunderte Millisekunden Spinner, Skeletons oder eine eingefrorene UI entstehen
- Linear legt die tatsächliche Datenbank, aus der die UI liest, in die IndexedDB des Browsers; Änderungen werden zuerst lokal angewendet, dann asynchron an den Server gesendet, und der Server überträgt Deltas per WebSocket an andere Clients
- Der größte Engpass schneller Webapps ist das Netzwerk; die Datenübertragung zwischen Client und Server verursacht Kosten von Hunderten Millisekunden
- Der Kernansatz von Linear besteht darin, Netzwerkanfragen für den Nutzer unsichtbar zu machen und Ladezustände nach Möglichkeit zu eliminieren
// Linear
issue.title = "Faster app launch";
issue.save();
issue.title = "Faster app launch" aktualisiert den In-Memory-Datenspeicher; bei Linear werden dafür beobachtbare MobX-Objekte genutzt
issue.save(); bedeutet, dass die Synchronisations-Engine dies im Batch verarbeitet und eine Transaktion in die Queue legt, die zum Server geflusht wird
- Die UI wird synchron anhand der lokalen Änderungen im Arbeitsspeicher neu gerendert, während die Datensynchronisierung im Hintergrund läuft, sodass kein Spinner nötig ist
- Tuomas sagte 2024 auf einer Konferenz, dass der erste Code, den er bei Linear schrieb, die Synchronisations-Engine war, und bezeichnete das als einen für Startups unüblichen Ansatz
- Die meisten Apps müssen keine eigene Synchronisations-Engine wie Linear bauen; schon mit optimistischen Updates von TanStack Query und SWR lässt sich eine sehr ähnliche wahrgenommene Geschwindigkeit erreichen
- Optimistische Requests bieten einen hohen Verbesserungseffekt, indem sie unnötige Spinner entfernen, Zustände sofort aktualisieren, im Hintergrund validieren und bei Bedarf ein Rollback durchführen
- Die Reaktionsfähigkeit der UI sollte nicht von Netzwerklatenz abhängen; die vom Nutzer empfundene Geschwindigkeit wird stärker von der Reaktion der Oberfläche als von der Antwortzeit des Servers bestimmt
-
Ein Blick auf Linears Stack
- Linear ist auf einem einfachen Stack aus React, TypeScript, MobX, Postgres und CDN aufgebaut
- Das Frontend verwendet React und
react-dom, MobX, TypeScript, Rolldown-Vite und plugin-react-oxc, ProseMirror und y-prosemirror, Radix UI primitives, Emotion und StyleX, Comlink, idb, graphql-request, Sentry sowie Inter Variable
- Das Backend nutzt Node.js und TypeScript, PostgreSQL auf Cloud SQL, Memorystore Redis, turbopuffer, Kubernetes auf GCP sowie Cloudflare Workers
- Der Desktop-Client basiert auf Electron, mobile Apps wurden für iOS in Swift und für Android in Kotlin separat vollständig neu implementiert
- Die Marketing-Website nutzt Next.js, styled-components und Inline-SVG-Sprites
- Linear bleibt bei Client-Side Rendering und zeigt, dass sich auch CSR mit der richtigen Architektur und dem richtigen Design unmittelbar anfühlen kann
- Wenn die gesamte App auf der Client-Seite bleibt, entsteht ein einfacheres mentales Modell, das Komplexität rund um die Trennung von Server und Client, die Verfügbarkeit von
window oder das Setzen von Cache-Headern reduziert
Den ersten Ladevorgang sofort wirken lassen
- Die Zeit bis zum eigentlichen Arbeitsbeginn ist bei Produktivitätstools ein wichtiges Detail
- Der initiale Ladevorgang einer Client-Side-App kann langsam werden, weil
index.html angefordert wird, dann JavaScript und CSS, dann die Authentifizierung verarbeitet wird und danach API-Anfragen zum Anzeigen der App folgen
-
Linears Bundler-Entwicklung: Parcel, Rollup, Vite, Rolldown
- Das Gefühl von unmittelbarer Geschwindigkeit beginnt schon vor der Laufzeit, nämlich zur Build-Zeit, und für schnelles Laden ist es entscheidend, die Menge an ausgeliefertem JavaScript und CSS zu reduzieren
- Linear hat seine Build-Pipeline in der Reihenfolge Parcel → Rollup → Vite → Rolldown neu geschrieben, wobei jeder Wechsel auf weniger JavaScript/CSS und eine bessere Developer Experience abzielte
- Verbesserungswerte laut Linear-Blog
- 50 % weniger ausgelieferter Code
- 30 % kleinere Größe nach der Komprimierung
- 10–30 % bessere Seitenladezeit bei Cold Cache
- 59 % weniger Time-to-first-paint in der active-issues-Ansicht unter Safari
- 70–80 % geringerer Speicherverbrauch
- Ein großer Teil der Verbesserungen kommt aus der Kombination, nur moderne Browser zu unterstützen, besserer dead-code elimination und aggressivem Code-Splitting
- Der Verzicht auf Legacy-Support bringt große Vorteile, weil dadurch Polyfills, ES5-Transpiling und
nomodule-Fallbacks entfallen
- Auch nach den Optimierungen liefert Linear noch etwa 21 MB minifiziertes JavaScript aus, teilt es aber aggressiv in Hunderte routebasierte Chunks auf, die nur bei Bedarf geladen werden
- Entscheidend ist nicht die Wahl eines bestimmten Bundlers, sondern das Entfernen von Legacy-Browsern, der Wechsel zu nativem ESM und aggressives Code-Splitting
- Diese Schritte summieren sich: Das JavaScript für den ersten Ladevorgang wurde bei Linear ungefähr halbiert, und die Build-Zeit sank nicht nur um einen einstelligen Faktor, sondern um eine ganze Größenordnung
-
Preload nach dem initialen Laden
- Wenn JavaScript in kleine Chunks aufgeteilt wird, entsteht leicht ein Wasserfallproblem, weil jeder Chunk andere Chunks importiert
- Linear sorgt dafür, dass der Browser die vollständige Liste schon vor der JavaScript-Ausführung sieht und Anfragen parallel starten kann, sodass die zugehörigen Chunks bereits im Cache liegen, wenn das Entry-Script das erste
import erreicht
- Durch das Abstimmen von
modulepreload im <head> und dem crossorigin-Wert des Entry-Scripts behandelt der Browser Preload und Import nicht als getrennte Ressourcen, sondern verwendet den gecachten Fetch erneut
- Die Cold-Load-Timeline verwandelt sich von einem sequenziellen Wasserfall in einen einzigen parallelen Batch; die Netzwerkarbeit bleibt, wird aber auf einmal erledigt
- Diese Arbeit läuft im Hintergrund, sobald der Nutzer erstmals auf der Login-Seite ankommt, und einige Sekunden später liegt die gesamte App im Cache und kann sofort ausgeliefert werden
-
Service Worker für mehr Geschwindigkeit und Offline-Funktionen
- Routebasierte Chunks für Views, die der Nutzer noch nicht besucht hat, werden vom Service Worker im Hintergrund gecacht
- Der Service Worker hat ein im Quellcode eingebettetes Precache-Manifest, und rund 1.200 gehashte Assets decken Route-Chunks, Icons und Schriftarten ab
- Wenige Sekunden nach Erreichen des Login-Bildschirms befindet sich die gesamte App im Cache
- Spätere Navigation überspringt das Netzwerk vollständig, und der Service Worker antwortet direkt aus seinem eigenen Cache, ohne den HTTP-Cache zu durchlaufen
- In Kombination mit der lokal bevorzugten Sync-Engine und den bereits in IndexedDB gespeicherten Nutzerdaten bleibt Linear auch offline nutzbar
- Unterstützt werden das Lesen von Issues, das Erstellen neuer Issues, das Bearbeiten von Titel und Beschreibung sowie Statusänderungen
- Alle Aktionen werden in einem lokalen Transaktionsspeicher in eine Queue gestellt und bei wiederhergestellter Verbindung ausgeführt
modulepreload holt das, was jetzt gebraucht wird, parallel, damit der Browser nicht an einer seriellen Import-Kette hängen bleibt
- Der Service Worker bereitet vor, was als Nächstes gebraucht wird
- Linears schneller Ladepfad besteht aus dem Entfernen von möglichst viel Code, dem Aufteilen in kleine Teile und Precache im Hintergrund; das Ziel ist, Netzwerkanfragen entweder schneller zu machen oder ganz zu eliminieren
-
Aufbau des Vendor-Bundles
- Jedes von Linear verwendete Paket wird in einen separaten Chunk aufgeteilt und unabhängig gecacht
- Ein traditionelles
vendor.js invalidiert den Cache des gesamten Dependency-Graphen, selbst wenn nur eine einzige Abhängigkeit aktualisiert wird
- Das Chunk-Splitting von Linear sorgt statt einer einzelnen großen Datei für feingranulares Vendor-Caching; bei einem Update einer bestimmten Abhängigkeit wird nur dieser eine Chunk invalidiert, während der Rest im Cache bleibt
-
Laden großer Schriftdateien
- Falsches Font-Loading kann kurzzeitig unsichtbaren Text, Layout Shifts beim Austausch gegen die eigentliche Schrift und doppelte Fetches durch nicht passende Preloads verursachen
- Linear lädt die Schrift Inter Variable im
<head> per Preload und nutzt Preconnect zu 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>
- Die Variable Font deckt die gesamte Weight-Achse von 100 bis 900 in einer einzigen woff2-Datei ab und vermeidet so Anfragen pro Gewichtung
font-display: swap rendert sofort den Fallback-Stack und ersetzt ihn, sobald Inter geladen ist
- Das
crossorigin="anonymous" im Preload-Tag ist die entscheidende Einstellung dafür, dass der Browser die gecachte Ressource wiederverwendet, wenn das CSS später auf dieselbe Schrift verweist
- Ohne
crossorigin unterscheiden sich der CORS-Modus des Preloads und der CSS-Referenz, wodurch der Browser die Schrift erneut laden muss
-
Inline-App-Shell
- Linear fügt im
<head> genug CSS inline ein, um den Ladezustand zu zeichnen, und kann so die App-Shell ohne Anfrage an ein externes Stylesheet anzeigen
- Inline-JavaScript führt sofort die für das initiale Erlebnis nötigen Verzweigungen aus
- Electron und der Linear User Agent werden erkannt, um die Klasse
electron hinzuzufügen
- Wenn
localStorage.ApplicationStore fehlt, wird die Klasse logged-out hinzugefügt
- Aus
localStorage.splashScreenConfig werden Shell-Token wie Sidebar-Hintergrund, Sidebar-Breite und Dark Mode wiederhergestellt
- Wenn der Nutzer eingestellt hat, Links in der Desktop-App zu öffnen, wird die Sidebar-Breite auf
8px gesetzt
- Noch bevor das erste JavaScript-Bundle über das Netzwerk eintrifft, ist der Ladebildschirm bereits passend zu Login-Status, Theme, Größe und Position eingerichtet
- Der schnellste Weg, damit die App direkt nach dem Drücken von Enter auf eine URL schon bereit wirkt, ist, die App-Shell bereits mit der ersten
index.html-Antwort mitzuliefern
-
Erst rendern, später authentifizieren
- Ein typischer Authentifizierungsablauf folgt der Reihenfolge HTML-Fetch, Bundle-Laden, Session-Prüfung, User-Fetch, Workspace-Fetch, Rendern, sodass es 1 bis 3 Sekunden dauern kann, bis der Nutzer überhaupt etwas sieht
- Linear behandelt Authentifizierung ähnlich wie Änderungsverarbeitung: Der Normalfall wird angenommen und die Prüfung läuft im Hintergrund
- Die meisten CRUD-Apps halten die eigentliche Session in einem HttpOnly-Cookie und ergänzen ein separates, von JavaScript lesbares Cookie oder eine
/me-Anfrage, damit das Frontend beim Start weiß, ob der Nutzer eingeloggt ist
- Das inline eingebettete Boot-Script von Linear prüft statt paralleler Authentifizierungssignale nur, ob
localStorage.ApplicationStore existiert
if (localStorage.getItem("ApplicationStore") === null) {
document.documentElement.classList.add("logged-out");
}
- Wenn
ApplicationStore vorhanden ist, hat der Nutzer Linear in diesem Browser schon einmal verwendet, und Workspace-Daten liegen bereits in IndexedDB
- Fehlt der Wert, gibt es keine Daten zum Rendern, daher wechselt die Shell in das
logged-out-Layout und der Login-Flow setzt ein
- Das eigentliche Session-Token liegt im Cookie, und das Bundle trifft keine Vorabentscheidung über den Session-Status
- Wenn beim WebSocket-Handshake, bei einem Sync-Delta oder bei einem HTTP-Aufruf wegen einer abgelaufenen Session ein 401 zurückkommt, leitet der Client zum Login weiter
- Das gesamte Muster vertraut lokalen Daten für sofortiges Rendern, nutzt den Server als Quelle der Korrektheit und gleicht beides asynchron miteinander ab
Synchronisations-Engine
- Linears Geschwindigkeit beginnt mit der Entscheidung, den Server nicht als source of truth der UI zu betrachten, sondern als Sync-Ziel
- Geschwindigkeit ist nicht das Ergebnis eines einzelnen Elements, sondern von drei ineinandergreifenden Achsen
-
1. Die Daten sind bereits da
- Beim Start der App wird der Workspace nicht vom Server geholt, sondern aus IndexedDB in einen In-Memory-Objektpool von MobX hydriert
- Alle Queries der UI zielen zuerst auf den Objektpool, und weil die Issues bereits auf dem Gerät des Nutzers vorhanden sind, gibt es keinen Zustand wie „Issues werden geladen“
- Mit dem Wachstum hat Linear die Daten der Synchronisations-Engine nach einem ähnlichen Prinzip wie bei JavaScript-Bundles in Chunks aufgeteilt
- Die beiden schwersten Tabellen, Issue und Comment, werden nicht auf einmal geladen, sondern bei Bedarf lazy-hydrated
- Das ist Code-Splitting auf Datenebene und sorgt dafür, dass die Startkosten nicht der Größe des Workspaces folgen, sondern seiner Struktur
- Selbst ein Workspace mit 10.000 Issues startet fast genauso schnell wie ein Workspace mit 100 Issues
- Öffnet man ein Projekt, sind die Issues bereits da, und filtert man nach Assignee, sind die Indizes schon aufgebaut
-
2. Änderungen warten nicht auf das Netzwerk
- Wenn sich der Status eines Issues ändert, passieren fast gleichzeitig drei Dinge
- Ein MobX-observable-Update spiegelt die Änderung in der UI wider
- Die Änderung wird in einer dauerhaften Transaktions-Queue in IndexedDB protokolliert
- Die Änderung wird zur Queue für die Übertragung an den Server hinzugefügt
- Zu diesem Zeitpunkt wird das Netzwerk noch gar nicht verwendet
- Nutzer müssen nicht warten, um ihre eigenen Änderungen zu sehen; Retry, Rollback und Reload über Neustarts hinweg werden alle im Hintergrund verarbeitet
- Wenn der Server ablehnt, wird das Observable zurückgesetzt und es kommt zu einem kurzen Flicker, aber die meisten ungültigen Änderungen werden schon vor dem Erzeugen der Transaktion abgefangen
- Bei Linear beginnt der Ablauf mit der lokalen Änderung und behandelt den Server nicht als Genehmigungsstufe, sondern als Bestätigungsstufe
-
3. Ein Delta, eine Zelle
- Wenn der Server die Änderung des Nutzers oder die einer anderen Person bestätigt, kommt ein kleines JSON-Envelope an den Client zurück, das zeigt, was sich geändert hat
- Der Client wendet die Änderung an, indem er Werte in das entsprechende MobX-Observable schreibt
- Alle Modelleigenschaften in Linear sind jeweils observables, und alle Komponenten, die diese Eigenschaften lesen, sind mit
observer() umschlossen
- MobX kann genau erkennen, welche Komponenten von welchen Feldern abhängen
- Eine Änderung an einem Feld eines Issues rendert nur die Komponenten neu, die dieses Feld lesen, nicht die übergeordnete Liste oder die gesamte Sidebar
- 50 aktualisierte Issues bedeuten nicht ein Re-Rendering der ganzen Liste, sondern Re-Rendering von 50 Zellen
- Selbst in einem geschäftigen Workspace, in dem 10 Personen gleichzeitig editieren, steigen die Kosten für eingehende Updates nicht mit allen sichtbaren Einträgen auf dem Bildschirm, sondern mit den Einträgen, die sich tatsächlich geändert haben
-
Warum diese drei zusammenwirken
- Eine lokale Datenbank ohne optimistische Writes führt beim Speichern weiterhin zu einem Spinner
- Optimistische Writes ohne fein granulare observables führen bei jedem Update weiterhin zu Rucklern
- Fein granulare observables ohne lokale Datenbank führen beim initialen Laden weiterhin zu Wartezeit
- Linears Geschwindigkeit ist keine Eigenschaft einer einzelnen Schicht, sondern des Gesamtsystems
- Bundler und Loader-Shell lassen den ersten Paint schnell wirken, und die Synchronisations-Engine sorgt dafür, dass es sich auch nach dem Start schnell anfühlt
Design für Geschwindigkeit
- Geschwindigkeit ist sowohl ein Engineering- als auch ein Designproblem
- Wenn der schnellste Aktionspfad Maus, drei Menüs und Klicks verlangt, zahlen Nutzer diese Kosten in Form von Schritten, unabhängig davon, wie schnell die Engine intern ist
- Eine weitere Achse von Linears Geschwindigkeit ist die Integration der Tastatur als primäres Werkzeug für Navigation und das Ausführen von Aufgaben
- Für alle häufigen Aufgaben gibt es Shortcuts, die Command Palette öffnet sich mit einem einzigen Tastendruck, und das Right-Click-Menü ist individuell gebaut
-
Jede Aktion hat einen Shortcut
- Einzelne Zeichen bearbeiten das fokussierte Issue, Zwei-Zeichen-Kombinationen dienen der Navigation, und Modifier werden für globale Aktionen verwendet
- Schon in Linears früher Phase waren Shortcuts ein grundlegendes Element, und die Synchronisations-Engine ist teilweise darauf ausgelegt, dass jede Aktion jederzeit ausgeführt werden kann
- Überall in der UI sind Shortcuts sichtbar, und die am häufigsten genutzten Shortcuts bestehen aus einem einzelnen Zeichen
- Damit Einsteiger nicht ausgeschlossen werden, kann jede Aktion auch mit der Maus ausgeführt werden
-
Die Command Palette ist immer nur einen Tastendruck entfernt
⌘ k öffnet die Command Palette, in der fast alle Aktionen in Linear durchsucht werden können
- Durchsuchbar sind Issues, Projekte, Labels, Statusänderungen, Navigation, das Erstellen von Issues, Einstellungen, Theme-Toggle und mehr
- Die Command Palette durchsucht nicht den Server, sondern den lokalen MobX-Objektpool und ist deshalb sehr schnell
- Die gesamte App ist in einem einzelnen Pane zugänglich, und Navigation, Erstellen von Issues und Statusänderungen laufen alle über die Suche
- Die Command Palette passt sich dem aktuellen Arbeitskontext an und vermittelt zugleich die zentralen Aktionen und Shortcuts jeder View
- Eine schnelle App braucht sowohl exzellentes Engineering als auch exzellentes Design: Engineering-Geschwindigkeit macht die einzelne Interaktion schnell, Design-Geschwindigkeit verkürzt den Weg bis zur Interaktion
- In einem Tool, das man den ganzen Tag nutzt, summiert sich der Unterschied zwischen einem Shortcut und einem Mauspfad von zwei Sekunden bei jeder einzelnen Aktion
Animationen
- Schlechte Animationen können die Millisekunden, die durch die Optimierung von initialem Laden, Updates und Datenbankabfragen eingespart wurden, im letzten Schritt wieder zunichtemachen
- Ein Element wie eine 500-ms-
height-Animation kann die Bemühungen untergraben, den Nutzer nicht warten zu lassen
-
Es gibt nur wenige Eigenschaften, die animiert werden sollten
- Änderungen von Properties im Browser verursachen je nach ihrer Position in der Rendering-Pipeline drei Kostenklassen
- Die composited Properties
transform und opacity verlagern die Arbeit auf die GPU und laufen unabhängig vom Main Thread
- Paint-auslösende Properties wie
color, background-color, border-color, fill überspringen das Layout, führen aber zu einem Neuzeichnen der Pixel
- Layout-auslösende Properties wie
width, height, top, left, margin, padding erzwingen eine Neuberechnung der Position aller nachfolgenden Elemente und sollten nicht animiert werden
/* Linear-Ansatz */
.row:hover {
background-color: var(--color-bg-hover);
transition: background-color 0.12s;
}
.icon-arrow {
transform: translateX(0);
transition: transform 0.15s;
}
- Wenn man
margin-left animiert, wird bei allen Rows unterhalb der gehoverten Row während der gesamten 200 ms der Transition in jedem Frame das Layout neu berechnet
- In langen Issue-Listen macht genau dieser Unterschied den Unterschied zwischen flüssiger Darstellung und Jank aus
- Die Animationseigenschaften von Linear sind größtenteils composited Properties wie
transform und opacity, gelegentlich auch background-color und border-color
-
Man muss wissen, wann Zurückhaltung angebracht ist
- In Tools, die täglich genutzt werden, können Animationen, die auf Marketing-Websites gut aussehen, die Arbeit stören
- Schon eine kleine Hover-Verzögerung an der falschen Stelle kann auffallen
- Viele Animationen von Linear wirken effektiv, weil sie sich auf ihren Ursprung beziehen
- Das Status-Popover skaliert aus der Status-Pill heraus, und das Agent-Panel gleitet aus dem Toggle herein
- Solche Bewegungen sind kein dekoratives Fade, sondern erfüllen eine räumliche Funktion, indem sie zeigen, woher ein neues Element kommt
-
Dauer kurz und unmittelbar halten
--speed-highlightFadeIn: 0s;
--speed-highlightFadeOut: .15s;
--speed-quickTransition: .1s;
--speed-regularTransition: .25s;
--speed-slowTransition: .35s;
- Viele Design-Systeme setzen die Standarddauer unnötig lang an
- Materials Standarddauer liegt bei 200 ms, und die iOS-Spring-Animation liegt näher bei 350 ms
- Die Standardwerte von Linear liegen auf der kürzeren Seite der Branchenpraxis
- Linear verwendet asymmetrisches Timing für Enter und Exit
- Hover-Highlight, Popover und Agent-Panel erscheinen beim Aufruf sofort und blenden beim Schließen über 150 ms aus
- Das Agent-Fenster erscheint sofort und blendet ähnlich wie unter macOS aus
Wie Linear schnell ist
- Die Performance von Linear beruht nicht auf einem einzelnen Geheimnis oder einer einzelnen Technik, sondern ist das Ergebnis von Hunderten richtiger Entscheidungen
- Große Teile des Ansatzes sind einfach und das Resultat davon, früh eine zur Nutzung passende Architektur festgelegt und beibehalten zu haben, ganz ohne Next, TanStack oder schillernde Frameworks
- Der Server fungiert nicht als Source of Truth der UI, sondern als Sync-Ziel
- Die Datenbank befindet sich im Browser, und Änderungen werden zuerst lokal angewendet und dann im Hintergrund abgeglichen
- Beim ersten Laden wird weniger Code in mehr Teilstücken ausgeliefert, und der Service Worker precacht den Rest, während sich der Nutzer auf der Login-Seite befindet
- Die Authentifizierung geht auf Basis des lokalen Zustands vom Normalfall aus und validiert später
- Die Sync-Engine hydriert aus IndexedDB in per-Property-MobX-Observables, sodass ein Update von 50 Issues nicht ein Re-Rendering der gesamten Liste, sondern Re-Renderings von 50 Zellen auslöst
- Das Eingabemodell ist keyboard-first, und für alle üblichen Aufgaben gibt es Shortcuts und eine globale Command Palette
- Animationen bleiben bei GPU-freundlichen Properties, und layout-auslösende Properties werden nicht animiert
- Der schwierige Teil ist weniger die Implementierung selbst als die Haltung, über Jahre hinweg auf Detailqualität zu achten, während die Codebasis reift, skaliert und auf neue Einschränkungen trifft
1 Kommentare
Hacker-News-Kommentare
Wenn man so eine Erfahrung in eine Anwendung einbauen möchte, lohnt sich ein Blick auf Zero(https://zero.rocicorp.dev/)
Live-Demo: https://gigabugs.rocicorp.dev/
Eine Liste mit Alternativen gibt es auch hier: https://zero.rocicorp.dev/docs/when-to-use#alternatives
Wenn einen die interne Funktionsweise interessiert, sind auch die Replicache-Design-Dokumente lesenswert: https://doc.replicache.dev/concepts/how-it-works
Replicache ist der Vorgänger von Zero, und das Kernprotokoll funktioniert noch immer nach demselben Prinzip
Neben dem klaren Performance-Vorteil dadurch, dass die Daten mit dem Client synchronisiert werden, war ich auch überrascht, wie viel einfacher der React-Code wird. Mit einer Sync-Engine verschwindet der Großteil des Client-Status, und man kann den größten Teil des Komponenten-Codes synchron denken
Ohne ein dediziertes Team aufzubauen, ist das wahrscheinlich die naheliegendste Option
Ich habe immer gehört, dass Linear schnell sei, aber nachdem ich es tatsächlich täglich genutzt habe, ist meine Begeisterung verflogen. Die Suche ist ziemlich langsam, die UI wirkt oft träge, und „Pulse“ sieht zwar gut aus, ist aber selbst in kleinem Maßstab wie eine Flut aus Rauschen
Es ist schwer, das Nötige zu finden, sodass ich am Ende alles zu den Favoriten hinzufüge. Das frühe Trello war als Projekt-Tracking-Erlebnis mit Abstand am besten
Letztes Jahr hat jemand die Linear-Sync-Engine rückentwickelt, auf GitHub veröffentlicht und eine tolle Erklärung dazu geschrieben
https://github.com/wzhudev/reverse-linear-sync-engine/blob/m...
Solche lokal priorisierten Synchronisations-Web-Apps sind wirklich interessant und können nützlich sein, aber ich halte die Prämisse für etwas falsch
Gemeint ist die Annahme wie: „In Linear reichen wenige Millisekunden, um ein Issue zu aktualisieren. Eine traditionelle CRUD-App braucht für dieselbe Aufgabe etwa 300 ms“ oder „Alle Daten, die zwischen Client und Server hin- und hergehen, kosten Hunderte Millisekunden“
Das Problem, dass die Round-Trip-Zeit zwischen HTTP-Client und Server wegen der Lichtgeschwindigkeit ansteigt, lässt sich zwar nicht lösen, aber man kann das Backend nah am Nutzer platzieren und schnell machen
Zum Beispiel ist es durchaus möglich, ein Web-App-Backend zu betreiben, das für die meisten Nutzer innerhalb von etwa 10 ms Round-Trip-Zeit liegt, und das Backend auch innerhalb von etwa 10 ms eine Antwort rendern zu lassen. Das heißt, auch eine traditionelle CRUD-App kann dieselbe Aufgabe nicht in 300 ms, sondern in etwa 30 ms erledigen
Es kann gut sein, dass Linear im Backend aus legitimen Gründen mehr Zeit braucht und deshalb Hilfe vom Frontend benötigt, aber das lässt sich nicht verallgemeinern. Jedes Stück JavaScript hat schließlich auch seinen eigenen Preis
us-west-1 liegt bei 60 ms, eu-centra-1 bei 100 ms, und Asien ist 200 ms entfernt. Dabei handelt es sich sogar um Rechenzentrumsverkehr; auf dem öffentlichen Internet bis zu Heimanschlüssen ist die Latenz deutlich schlechter
Die Datenbank muss sich in genau einer Region befinden. Wo auch immer man sie platziert, die Mehrheit der Nutzer auf der Erde wird mehr als 100 ms davon entfernt sein
Dass es egal sei, wo sich der Endpunkt befindet, stimmt nicht, denn der Endpunkt muss mit der Datenbank kommunizieren, um Daten zu lesen und zu schreiben. In dem Moment, in dem man versucht, Daten in Nutzernähe zu replizieren, besitzt man am Ende eine lokal priorisierte Synchronisationsdatenbank
Ob man sie selbst baut oder ein fertiges Produkt nimmt, diese replizierte Datenbank bringt alle gleichen Probleme wie clientseitige Synchronisation mit sich, und erhebliche Netzwerklatenz bleibt trotzdem bestehen. Die Physik lässt sich nicht austricksen, daher bleibt für die meisten Nutzer nur die Wahl zwischen einem 0,25-Sekunden-Commit oder Eventual Consistency, also Synchronisation
Natürlich kann man so etwas wie ein „Zwischen-Backend“ in ein weltweites CDN-Edge-Netz verlagern, aber ab diesem Punkt bezahlt man denselben Komplexitätspreis wie bei diesem Ansatz, das „Zwischen-Backend“ in den Client zu legen
Im schlimmsten Fall gibt der Background-Worker eine Meldung über ein fehlgeschlagenes Update aus, die der UI-Thread empfängt und anzeigt. Der Erfolgsfall bleibt weiterhin blitzschnell
Eine eventually consistent Datenbank zu schreiben, ist schwierig, und für den Anwendungsfall von Linear mag das in Ordnung sein, aber nicht zu wissen, ob mein Update den Server, also das Team, erreicht hat, ist ein Problem
In anderen Projekten, an denen ich früher beteiligt war, haben Synchronisationsverzögerungen unzählige Probleme verursacht, deshalb habe ich mich immer für synchrone Lösungen entschieden. Schicke Features hole ich nur hervor, wenn sie wirklich nötig sind; lieber optimiere ich den Server extrem schnell und lasse die Nutzer stattdessen mit der Netzwerklatenz leben
Wir nutzen Linear im Unternehmen. Ich weiß, dass ich damit in der Minderheit bin, aber die User Experience ist wirklich anstrengend. Schnell würde ich es auch kaum nennen
Die Seite selbst lädt technisch gesehen einigermaßen schnell, aber etwa die Hälfte der Zeit sieht man nur, wie sich Zahlen auf der Seite ändern, ohne irgendeinen visuellen Hinweis darauf, dass Daten noch geladen werden
Es ist so schlimm, dass ich in Linear nur Issues mit einer einzeiligen Beschreibung anlege und die Details dann in GitHub ausfülle. Genau darin ist Linear gut und schnell
Leider ist das für ein Unternehmen, das überleben und in den oberen Markt aufsteigen will, praktisch der einzige Weg
Das Wort „schnell“ würde ich nicht verwenden. Wenn das Laden schon 30 Sekunden dauert, ist es nicht besonders wichtig, ob Issue-Updates von 300 ms auf „ein paar“ Millisekunden sinken
Besser als Jira, aber das ist eine sehr niedrige Messlatte
Cool. Vielleicht kann ich etwas Ähnliches auch in das Browser-Spiel und die Engine einbauen, an denen ich arbeite, sodass sich nach dem ersten Laden Ladezustände vollständig vermeiden lassen. Bei mir ist es eine serverlose, vollständig clientseitige Struktur mit statischen Assets
Ich bin von der Performance dieses Spiels besessen. Vor letztem Wochenende habe ich noch damit gekämpft, auf einem M1 MacBook Pro 128 gleichzeitige Spieler zu simulieren, dabei Pfadsuche, schwere Strategielogik und Rendering komplett innerhalb des Viewports auszuführen und trotzdem 120 fps zu halten; nur ganz gelegentlich gab es Framedrops, und die Frame-Zeit lag bei etwa 4 ms
Über das Wochenende habe ich massiv Performance-Arbeit geleistet, und jetzt kann ich 2048 gleichzeitige Spieler mit einer Frame-Zeit von unter einer Millisekunde simulieren. Das schließt Rendering, sämtliche Logik und auch prozedurale Generierung ein
Ich habe außerdem 11,2-faches CPU-Throttling aktiviert, um ein schwaches Mobilgerät zu simulieren, und selbst dann läuft es bei 256 bis 512 gleichzeitigen Spielern mit etwa 5 ms Frame-Zeit stabil bei 60 fps. Der Hauptengpass sind jetzt ein paar kleinere Logikprobleme und die Start-/Boot-Zeit, die auf schwachen Geräten verbessert werden muss, und ich glaube, von Linear kann man da etwas lernen
Ich hatte eigentlich immer das Gefühl, dass Linear ziemlich langsam ist. Es gab Wochen, in denen ein offener Tab irgendwann mit 100 % CPU lief
Interessant ist es schon. Ehrlich gesagt habe ich Linear nie als „schnell“ wahrgenommen. Wie die meisten Web-Apps hatte es eine gewisse Latenz, aber verglichen mit JIRA ist es natürlich Lichtgeschwindigkeit
Linear selbst ist großartig, und nach der JIRA-Folter ist es wirklich erfrischend. Wenn man über optimistisches Routing und „schnell“ sprechen will, sollte man vielleicht zuerst über Gmail reden
Die Antwort auf Geschwindigkeit ist Prefetching. Im Grunde lädt man bei der Initialisierung eine Client-Datenbank herunter und hat dann eine Strategie zur Cache-Invalidierung
Um den Datensynchronisationsaspekt dieses Paradigmas umzusetzen, habe ich starfx gebaut: https://starfx.bower.sh/learn#data-loading-strategy-stale-wh...