Fallstudie

SPA vs. Hypermedia: Ein kollaboratives Kanban-Board im Vergleich mit PLANKA

Wir haben ein Kanban-Board gebaut, auf dem mehrere Leute gleichzeitig Karten ziehen, komplett auf dem Server gerendert, und es auf einem gedrosselten Handy gegen PLANKA gemessen.

Das Kanban-Board mit 200 Karten, eine Karte wird gerade gezogen
Kurzzusammenfassung

Wir haben ein Board im Stil von Trello gebaut, auf dem mehrere Leute gleichzeitig Karten ziehen, komplett auf dem Server gerendert: ein Go-Binary, eine SQLite-Datei, Datastar im Browser. Gemessen haben wir es gegen PLANKA Community 2.2.1, eine verbreitete selbst gehostete Kanban-App mit React, Redux, Sails.js und PostgreSQL. Beide hatten dieselben Boards, und die gemessene Person sass an einem gedrosselten Handy hinter 150 ms Round Trip.

Unser Board überträgt 58 kB und steht nach 0.6 Sekunden auf dem Bildschirm; PLANKA überträgt 2.6 MB und zeigt sein Board nach 15.4 Sekunden. Eine fallen gelassene Karte steht nach 252 ms an ihrem neuen Platz, vom Server schon angenommen; PLANKA zeigt die verschobene Karte nach 481 ms und hat die Bestätigung des Servers nach 712 ms. Unser Board zeigt nie eine verschobene Karte, die der Server nicht angenommen hat. In einem Punkt ist PLANKA schneller: beim Kommentieren.

Warum ein Kanban-Board

Unser Vergleich mit dem KI-Chat lässt einen Einwand offen: Vom Server gerendertes HTML mag für Formulare und Chats reichen, aber eine App mit Drag and Drop, in der mehrere Leute gleichzeitig denselben Bildschirm bearbeiten, braucht eine Single-Page-App. Ein Kanban-Board ist genau so eine App. PLANKA ist ein echtes Produkt, als Single-Page-App gebaut, mit rund 12’600 Sternen auf GitHub.

Die beiden Apps

Beide Apps können, was wir verglichen haben: anmelden; Listen anlegen, umbenennen, umsortieren und archivieren; Karten anlegen, umbenennen, verschieben und archivieren, mit der Maus und mit der Tastatur; eine Kartenansicht mit Beschreibung in Markdown, Labels, Fälligkeitsdatum und Kommentaren; und Zusammenarbeit live: Alle auf einem Board sehen jede Änderung und wer sonst gerade da ist.

PLANKA kann viel mehr: Mitglieder auf Karten, Suche und Filter, weitere Ansichten des Boards, einen Verlauf der Aktivitäten, Kommentare bearbeiten, Karten löschen, eine Stoppuhr, Anhänge, Aufgabenlisten, eigene Felder, Benachrichtigungen, Webhooks, eine REST-API und Zwei-Faktor-Login. Wir haben nur verglichen, was beide können, und PLANKAs Bundle und Speicher bezahlen auch für den Rest.

BereichUnser BoardPLANKA Community 2.2.1
ServerGo, ein BinaryNode.js mit Sails.js, socket.io
DatenbankSQLite, eine DateiPostgreSQL 16
BrowserDatastar 1.0.4, Starbase-KomponentenReact 18, Redux, redux-saga, react-beautiful-dnd
Live-UpdatesServer-Sent Events, HTMLWebSocket, JSON-Events

Wie das Board synchron bleibt

  1. BrowserJemand lässt eine Karte fallen. Die Seite schickt einen POST mit einer Operations-ID.
  2. WriterEine einzige Goroutine hat die Schreibverbindung und committet die wartenden Befehle in einer Transaktion. Der Request bekommt ein leeres 204.
  3. RendernDer Server rendert das Board höchstens alle 50 ms, einmal für alle, die es offen haben.
  4. Jede offene SeiteDer Stream jeder Seite bekommt das neue HTML, mit Brotli komprimiert, und Datastar morpht es in die Seite.
Eine Aktion, vom Fallenlassen bis auf jeden Bildschirm des Boards.

Jede Aktion auf unserem Board ist ein POST-Request, der die Datenbank ändert und mit einem leeren 204 antwortet. Jede offene Seite hält einen Stream mit Server-Sent Events, und nach jeder Änderung schickt der Server das Board als HTML durch diesen Stream. Karten, die sich nicht geändert haben, kommen aus einem Cache, dessen Schlüssel ihr Inhalt ist, und eine langsame Verbindung bekommt den neuesten Frame statt einer Warteschlange alter Frames. Ein Frame enthält nur die Listen, die sich geändert haben, und Brotli komprimiert über die Frames hinweg, deshalb kostet ein Frame nach einer verschobenen Karte weniger als ein Kilobyte auf der Leitung. Das Fenster von Brotli gehört zu einer Verbindung, also komprimiert jeder Stream jeden Frame für sich; alles andere an einem Frame passiert einmal, und höchstens die Hälfte der Prozessorkerne komprimiert gleichzeitig, damit kein Schwall von Frames eine Aktion aufhält.

Alle Schreibzugriffe laufen über die eine Goroutine mit der Schreibverbindung. Während eine Transaktion committet, sammeln sich neue Befehle, und die nächste Transaktion nimmt sie alle, jeden in seinem eigenen Savepoint, damit ein abgelehnter Befehl nur seine eigenen Änderungen zurücknimmt. Die Prüfung "hat jemand diese Karte verschoben, seit Ihre Seite gezeichnet wurde" läuft in diesem Befehl, ohne dass ein Lock über das Netzwerk gehalten wird. Das Design folgt Anders Murphys Messungen zu SQLite unter Konkurrenz. SQLite läuft mit synchronous=FULL, jeder Commit wartet also auf die Disk.

Eine fallen gelassene Karte bleibt, wo der Server sie zuletzt hingestellt hat, und sieht wartend aus (abgedunkelt, mit Spinner), bis der Frame ankommt, der sie am neuen Platz zeigt; dieser Frame nimmt auch das Wartende weg. Bis dahin öffnet das Board dort eine Lücke, wo die Karte fallen gelassen wurde, und zeigt darin eine gestrichelte Kopie: was die Person getan hat, nicht was der Server getan hat. Lehnt der Server das Verschieben ab, bleibt die Karte stehen, und die Seite sagt warum: "Anna hat ‘Fix login’ inzwischen nach ‘Done’ verschoben." Jede Aktion trägt eine ID, deshalb wirkt ein Request, den der Browser nach einem Netzwerkfehler zweimal schickt, nur einmal.

Wie wir gemessen haben

  • Dieselben Daten. Unser Seed legt vier Personen und zwei Boards an. Das gemessene, "Release 2.0", hat 8 Listen, 200 Karten und 268 Kommentare. Wir haben es exportiert und über die API in ein frisch installiertes PLANKA geladen, mit Labels, Fälligkeitsdaten und den Autorinnen und Autoren der Kommentare. Nach allen Läufen hatten beide Datenbanken noch dieselben Listen, Karten und Kommentare; 2 von 200 Karten lagen an einem anderen Platz, weil eine Karte leicht anders landet, wenn die Karten verschieden hoch sind.
  • Dieselbe Maschine. Beide Apps, die Datenbank und der Browser liefen auf einer Maschine (Intel i5-13500). PLANKA lief aus seinem offiziellen Docker-Image, mit einer Compose-Datei nach dem offiziellen Vorbild.
  • Dasselbe langsame Netz. Ein kleiner TCP-Proxy vor jeder App fügt 150 ms Round Trip hinzu und begrenzt die Bandbreite auf 1.6 Mbit/s nach unten und 750 kbit/s nach oben, für jedes Paket, auch für die auf unserem Stream und PLANKAs WebSocket. Chromes eigene Netzwerk-Drosselung verzögert einen Request beim Start und liess in unseren ersten Läufen Daten, die später auf einem offenen Stream ankamen, unverzögert durch.
  • Ein langsames Handy. Chrome hat die CPU der gemessenen Person viermal verlangsamt. Eine zweite Person schaute auf demselben Board mit normaler CPU zu.
  • Ein kalter Ladevorgang ist der erste Besuch einer angemeldeten Person: ein neues Browserprofil nur mit dem Session-Cookie, ohne Cache und ohne offene Verbindung.
  • Mit der Maus gezogen. Bei jedem Verschieben zogen wir eine Karte eine Liste nach rechts. PLANKAs Tastatur-Drag hat in unseren automatisierten Läufen keine Karten verschoben, deshalb haben wir beide Apps mit der Maus gemessen.
  • Je drei Läufe, in jedem 20 verschobene Karten. Beim Verschieben, Öffnen und Kommentieren legen wir die Messungen aller Läufe zusammen, das p95 jeder App beruht also auf 60 verschobenen Karten; Werte zum Laden sind Mediane der Läufe. Lighthouse lief dreimal pro Formfaktor, angemeldet, mit leerem Cache, und wir haben den mittleren Lauf behalten.

Das Board laden

App0.5 s1 s2 s4 s8 s16 s
Unser BoardUnser Board nach 0.5 s: eine leere weisse SeiteUnser Board nach 1 s: das ganze Board mit allen Listen und KartenUnser Board nach 2 s: das ganze BoardUnser Board nach 4 s: das ganze BoardUnser Board nach 8 s: das ganze BoardUnser Board nach 16 s: das ganze Board
PLANKAPLANKA nach 0.5 s: eine leere weisse SeitePLANKA nach 1 s: eine leere weisse SeitePLANKA nach 2 s: eine leere dunkle SeitePLANKA nach 4 s: eine leere dunkle SeitePLANKA nach 8 s: eine leere dunkle SeitePLANKA nach 16 s: das ganze Board mit allen Listen und Karten
Was das gedrosselte Handy beim kalten Laden des Boards mit 200 Karten zeigt: das letzte Bild, das Chrome bis zum jeweiligen Zeitpunkt gezeichnet hat. Die Bilder stammen aus einem eigenen Lauf mit Tracing, das die Seite etwas bremst; die Zeiten unten stammen aus Läufen ohne Tracing.
Kalter Ladevorgang, 200 KartenUnser BoardPLANKAUnterschied
Übertragen58 kB2’576 kB44× weniger
JavaScript übertragen40 kB2’226 kB56× weniger
Board auf dem Bildschirm (Largest Contentful Paint)0.6 s15.4 s27× früher
Alle Skripte geladen (Load-Event)0.6 s12.5 s20× früher
Layout-Verschiebung (CLS)00.02keine

PLANKA lädt seine ganze Anwendung herunter, bevor es nach dem Board fragt; auf einer langsamen Verbindung gehen deshalb die meisten der 15.4 Sekunden an 2.2 MB JavaScript. Unser Server schickt das Board als HTML in der ersten Antwort, und der Browser zeichnet es, sobald das Stylesheet da ist. Unser JavaScript lädt danach, fünf Dateien in einer Welle: Datastar und je eine Datei pro Starbase-Komponente. Drag and Drop funktioniert, sobald sie da sind, auf dieser Verbindung nach 0.6 Sekunden.

Unser Board nach 2 s: das ganze Board mit allen Listen und Karten
PLANKA nach 2 s: eine leere dunkle Seite
Zwei Sekunden nach dem Tippen auf den Link, auf dem gedrosselten Handy. Zum Vergleichen den Regler ziehen.

Lighthouse

MesswertUnser Board, DesktopPLANKA, DesktopUnser Board, MobilePLANKA, Mobile
Performance-Score100699833
First Contentful Paint0.3 s2.4 s1.4 s14.0 s
Largest Contentful Paint0.3 s2.6 s1.4 s14.9 s
Total Blocking Time0 ms140 ms160 ms990 ms

Lighthouse simuliert das langsame Netz und das langsame Handy selbst (Mobile: 150 ms Round Trip, 1.6 Mbit/s, 4× CPU) und rechnet mit einem eigenen Modell, wie sich Requests überlappen; seine Mobile-Werte weichen deshalb von unseren gemessenen ab. Mit einer Datei pro Komponente zählt es für unser Board auf Mobile 160 ms Total Blocking Time; mit denselben Komponenten als 17 einzelne Module waren es 80 ms.

Eine Karte verschieben

Unser Board
253 ms
PLANKA
712 ms
  • Die Karte bleibt stehen und sieht wartend aus; eine gestrichelte Kopie zeigt, wohin sie kommt
  • PLANKAs Drop-Animation
  • In der neuen Liste angezeigt, vom Server noch nicht angenommen
  • Am neuen Platz und vom Server angenommen
Vom Fallenlassen, bis der Server das Verschieben bestätigt hat, Median aus 60 verschobenen Karten auf dem gedrosselten Handy.
Mit der Maus eine Liste nach rechtsUnser BoardPLANKAUnterschied
Karte in der neuen Liste angezeigt252 ms481 ms1.9× früher
Verschieben vom Server bestätigt253 ms712 ms2.8× früher
Verschieben bestätigt, p95276 ms756 ms2.7× früher
Eine zweite Person sieht die verschobene Karte199 ms656 ms3.3× früher

"Angezeigt" heisst in den beiden Apps nicht dasselbe. Beide markieren das Ziel sofort: PLANKAs Liste öffnet schon beim Ziehen eine Lücke, in die seine Karte nach dem Loslassen schwebt, und unser Board öffnet eine Lücke mit einer gestrichelten Kopie der Karte. PLANKA verschiebt die Karte selbst im Browser und sagt es dem Server danach; seine Karte steht nach 481 ms in der Liste, am Ende seiner Drop-Animation, und die Bestätigung des Servers kommt nach 712 ms. Lehnt der Server das Verschieben ab, stand die Karte schon dort, wo sie nicht ist. Unser Board verschiebt die Karte erst, wenn der Frame des Servers sie bringt, nach 252 ms, und derselbe Frame nimmt das Wartende weg. PLANKA meldet das Verschieben erst nach seiner Drop-Animation an den Server, deshalb sieht auch die zweite Person die Karte später.

Unser Board 150 ms nach dem Loslassen: eine gestrichelte Kopie der Karte in der Lücke, die Karte selbst abgedunkelt in ihrer alten Liste
PLANKA 150 ms nach dem Loslassen: Die Karte schwebt noch in ihre Lücke
150 ms nach dem Loslassen, auf dem gedrosselten Handy. Unser Board: eine Lücke mit einer gestrichelten Kopie, wohin die Karte kommt, die Karte selbst noch abgedunkelt in ihrer alten Liste. PLANKA: Die Karte schwebt noch in ihre Lücke.

Einen Kommentar schreiben

Kommentar schreiben (p50)Unser BoardPLANKAUnterschied
Kommentar angezeigt302 ms90 msPLANKA 3.4× früher
Kommentar vom Server bestätigt302 ms229 msPLANKA 1.3× früher

Hier ist PLANKA schneller. Es zeigt einen neuen Kommentar sofort, bevor der Server ihn hat, und bekommt die Bestätigung nach 229 ms über seinen offenen WebSocket. Unser Board schickt einen HTTP-Request und zeigt den Kommentar mit dem nächsten Frame der Kartenansicht, den die gedrosselte CPU morphen muss, nach 302 ms.

Reaktionszeit und Speicher im Browser

Nach der SitzungUnser BoardPLANKAUnterschied
Interaction to Next Paint (INP)64 ms256 ms4× kürzer
JavaScript-Heap4.4 MB45.0 MB10× weniger
DOM-Elemente1’8433’2951.8× weniger
Karte öffnen258 ms318 ms1.2× früher

Viele Leute auf einem Board

Wir haben unser Board auch mit simulierten Personen belastet, jede mit einem echten komprimierten Stream, die nach festem Takt zufällige Karten auf dem Board mit 200 Karten verschieben. Das Tool misst, worauf eine Person wartet: von dem Moment, in dem sie eine Karte verschiebt, bis das Ergebnis auf ihrem eigenen Stream ankommt. Server und Lasttool liefen auf getrennten Kernen derselben Maschine; die Werte sind Mediane aus drei Läufen à 30 Sekunden.

Messwert50 Personen, jede verschiebt eine Karte pro Sekunde200 Personen, jede verschiebt vier Karten pro Sekunde
Verschobene Karten pro Sekunde48790
Als veraltet abgelehnt15 von 1’5084’425 von 23’967
Request beantwortet (204), p9514 ms12 ms
Ergebnis auf dem eigenen Stream, p9577 ms99 ms
Bytes pro Frame auf der Leitung532999
CPU des Servers1.6 Kerne4.9 Kerne
Speicher des Servers258 MB1.2 GB

"Als veraltet abgelehnt" heisst: Zwei Personen haben im selben Moment dieselbe Karte verschoben, und die zweite hat erfahren, wer sie wohin verschoben hat.

Die Datenbank ist nicht, womit der Server seine Zeit verbringt. Das Board zu rendern dauert etwa 2 ms pro Frame, einmal für alle. Bei 800 verschobenen Karten pro Sekunde ändert jeder Frame alle acht Listen, etwa 140 kB HTML, und jeder der 200 Streams komprimiert ihn mit seinem eigenen Brotli-Fenster: Das sind die meisten der fünf Kerne. Der Zustand des Kompressors ist auch der grösste Teil des Speichers, etwa 2.2 MB pro aktivem Stream, die Gos Garbage Collector ungefähr verdoppelt. Zweihundert Personen auf einem ruhigen Board brauchen 150 MB.

Der Writer allein

Ohne HTTP und Streams schafft unser Writer etwa 13’800 verschobene Karten pro Sekunde, wenn 200 Personen gleichzeitig verschieben, und 240 bis 340, wenn eine Person allein verschiebt. Zuerst waren es etwa 10’000: Der SQLite-Treiber in reinem Go bereitet eine Anweisung bei jedem Aufruf neu vor, und das kostete 41% der CPU des Writers. Jetzt bereitet der Writer jede Anweisung einmal vor und behält sie. Anders Murphy hat mit demselben Design 98’163 Transaktionen pro Sekunde gemessen. Zwei Dinge erklären den Unterschied:

  • Wer auf unserem Board eine Karte verschiebt, löst etwa fünfzehn SQL-Anweisungen aus: Operations-ID und Mitgliedschaft prüfen, zwei Savepoints, Karte und Liste lesen, die Position bestimmen, das Update, die Board-Version und die Operation festhalten. Seine Überweisung macht zwei Updates.
  • Auf seinem MacBook kehrt fsync bei SQLite zurück, bevor das Laufwerk seinen Cache geschrieben hat, ausser PRAGMA fullfsync ist eingeschaltet, was in seinen Einstellungen nicht steht. Auf unserer Linux-Maschine wartet ein Commit 3 bis 5 ms auf die Disk, und das ist die Grenze für eine Person, die allein verschiebt.

Ein Board kommt nicht in die Nähe dieser Grenze: Wenn 200 Personen alle 250 ms eine Karte verschieben, sind das 800 pro Sekunde.

Was auf dem Server läuft

Installierte Grösse
Unser Board: ein Binary, 13.5 MB
PLANKA: zwei Images, 673 MB
Speicher im Ruhezustand
Unser Board: 28 MB
PLANKA: 200 MB
Dieselben Boards auf der Disk
Unser Board: 143 kB
PLANKA: 10.3 MB
Prozesse
Unser Board: 1
PLANKA: 18
Im Ruhezustand nach den Messläufen. Jedes Paar ist im selben Massstab gezeichnet.
Im Ruhezustand nach den LäufenUnser BoardPLANKA
Was läuftein Binary mit 13.5 MB unter systemdzwei Container: PLANKA (Image 379 MB) und PostgreSQL 16 (Image 294 MB)
Prozesse118
Speicher28 MB200 MB
Dieselben Boards auf der DiskSQLite-Datei mit 143 kBPostgreSQL-Datenbank mit 10.3 MB
Abhängigkeiten12 Go-Module, einkompiliert83 Pakete im Client und 37 im Server, dazu deren Abhängigkeiten

Eine leere PostgreSQL-Datenbank belegt schon 7.3 MB, und PLANKAs ganzes Datenverzeichnis belegt 65 MB, das meiste davon Segmente des Write-Ahead-Logs und Template-Datenbanken. Der Speicher unseres Servers wächst mit den offenen Streams, wie oben beschrieben.

Ein Board mit 200 Karten schnell auf dem Handy

Die erste Version unseres Boards brauchte auf der gedrosselten CPU pro Frame etwa 120 ms im Main Thread, und jeder Tastendruck während eines Frames wartete darauf; das ergab ein INP von 248 ms. Drei Änderungen brachten einen Frame auf etwa 55 ms:

  • Ein Frame enthält nur die Listen, die sich geändert haben; die anderen gehen als leere Platzhalter mit, die Datastars Morph überspringt, weil beide Kopien data-ignore-morph tragen.
  • Ein Frame patcht die Listen innerhalb der Board-Komponente, nie das Element der Komponente selbst. Diese eine Änderung brachte einen Frame von etwa 100 ms auf etwa 55 ms.
  • Listen und Karten ausserhalb des Bildschirms haben content-visibility: auto, so berechnet und zeichnet der Browser die Listen nicht, die ein Handy gar nicht zeigt.

Mit der Standardeinstellung für Wiederholungen von Datastar 1.0.4 wird ein Stream, den der Server sauber schliesst, wie bei einem Deployment, nicht wieder geöffnet, und die Seite hört ohne jedes Zeichen auf, sich zu aktualisieren. Unser Board öffnet seinen Stream mit retry: 'always', ein Watchdog ersetzt eine Verbindung, die still geworden ist, und ein Banner sagt, wenn die Seite veraltet sein könnte.

Einschränkungen

  • Beide Apps liefen mit dem Browser auf einer Maschine, keine bezahlte also für echte Distanz zu einem Rechenzentrum. Der Proxy fügte beiden dieselbe Verzögerung hinzu.
  • Wir haben verglichen, was beide Apps können. PLANKA kann viel mehr, und ein Teil seines Gewichts bezahlt dafür.
  • Wir haben PLANKA Community 2.2.1 so gemessen, wie es ausgeliefert wird. Das kostenpflichtige PLANKA Pro 2.5.0 (September 2026) nennt Optimierungen für grosse Boards, die wir nicht getestet haben, und ein abgestimmtes Deployment mit CDN und HTTP-Caching würde schneller laden.
  • Das Verschieben haben wir nur mit der Maus gemessen.
  • Den Lasttest haben wir nur gegen unser Board gefahren, mit dem Lasttool auf derselben Maschine wie der Server, auf getrennten Kernen. Auf der Maschine laufen auch andere Dienste.

Wann was passt

Wenn Ihr Team heute ein selbst gehostetes Kanban-Board mit Anhängen, Aufgabenlisten, eigenen Feldern und Benachrichtigungen braucht: PLANKA hat sie, wir haben sie nicht gebaut. In dieser Fallstudie geht es darum, wie man interaktive Software baut. Ein Board mit Drag and Drop, Zusammenarbeit live und Konflikten zwischen Leuten passt in vom Server gerendertes HTML mit ehrlichen Wartezuständen, lädt auf einem langsamen Handy in deutlich unter einer Sekunde und bleibt reaktiv. Es läuft als ein Binary mit einer Datendatei. Wenn Sie gerade eine interaktive Anwendung bauen wollen und annehmen, dass sie eine Single-Page-App braucht, messen Sie zuerst die Version, die der Server steuert.

Selbst ausprobieren

Das Board läuft auf kanban.zweiundeins.gmbh. Melden Sie sich als anna@example.com, ben@example.com, chiara@example.com oder dev@example.com mit dem Passwort demo an, am besten in zwei Browsern gleichzeitig; die Boards gehen jede Nacht auf ihren Ausgangszustand zurück.

Quellcode, Messskripte, das PLANKA-Setup und alle Rohdaten liegen auf GitHub. go tool task dev startet das Board mit Demodaten; benchmarks/RESULTS.md beschreibt, wie man jede Messung in diesem Artikel wiederholt.

PLANKA ist eine Marke der PLANKA Software GmbH. Wir haben es unabhängig gemessen; dieser Artikel ist von ihnen weder geprüft noch unterstützt.