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.

- Warum ein Kanban-Board
- Die beiden Apps
- Wie das Board synchron bleibt
- Wie wir gemessen haben
- Das Board laden
- Eine Karte verschieben
- Einen Kommentar schreiben
- Reaktionszeit und Speicher im Browser
- Viele Leute auf einem Board
- Was auf dem Server läuft
- Ein Board mit 200 Karten schnell auf dem Handy
- Einschränkungen
- Wann was passt
- Selbst ausprobieren
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.
| Bereich | Unser Board | PLANKA Community 2.2.1 |
|---|---|---|
| Server | Go, ein Binary | Node.js mit Sails.js, socket.io |
| Datenbank | SQLite, eine Datei | PostgreSQL 16 |
| Browser | Datastar 1.0.4, Starbase-Komponenten | React 18, Redux, redux-saga, react-beautiful-dnd |
| Live-Updates | Server-Sent Events, HTML | WebSocket, JSON-Events |
Wie das Board synchron bleibt
- BrowserJemand lässt eine Karte fallen. Die Seite schickt einen POST mit einer Operations-ID.
- WriterEine einzige Goroutine hat die Schreibverbindung und committet die wartenden Befehle in einer Transaktion. Der Request bekommt ein leeres 204.
- RendernDer Server rendert das Board höchstens alle 50 ms, einmal für alle, die es offen haben.
- Jede offene SeiteDer Stream jeder Seite bekommt das neue HTML, mit Brotli komprimiert, und Datastar morpht es in die Seite.
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
| App | 0.5 s | 1 s | 2 s | 4 s | 8 s | 16 s |
|---|---|---|---|---|---|---|
| Unser Board | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() |
| PLANKA | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() |
| Kalter Ladevorgang, 200 Karten | Unser Board | PLANKA | Unterschied |
|---|---|---|---|
| Übertragen | 58 kB | 2’576 kB | 44× weniger |
| JavaScript übertragen | 40 kB | 2’226 kB | 56× weniger |
| Board auf dem Bildschirm (Largest Contentful Paint) | 0.6 s | 15.4 s | 27× früher |
| Alle Skripte geladen (Load-Event) | 0.6 s | 12.5 s | 20× früher |
| Layout-Verschiebung (CLS) | 0 | 0.02 | keine |
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.


Lighthouse
| Messwert | Unser Board, Desktop | PLANKA, Desktop | Unser Board, Mobile | PLANKA, Mobile |
|---|---|---|---|---|
| Performance-Score | 100 | 69 | 98 | 33 |
| First Contentful Paint | 0.3 s | 2.4 s | 1.4 s | 14.0 s |
| Largest Contentful Paint | 0.3 s | 2.6 s | 1.4 s | 14.9 s |
| Total Blocking Time | 0 ms | 140 ms | 160 ms | 990 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
- 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
| Mit der Maus eine Liste nach rechts | Unser Board | PLANKA | Unterschied |
|---|---|---|---|
| Karte in der neuen Liste angezeigt | 252 ms | 481 ms | 1.9× früher |
| Verschieben vom Server bestätigt | 253 ms | 712 ms | 2.8× früher |
| Verschieben bestätigt, p95 | 276 ms | 756 ms | 2.7× früher |
| Eine zweite Person sieht die verschobene Karte | 199 ms | 656 ms | 3.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.


Einen Kommentar schreiben
| Kommentar schreiben (p50) | Unser Board | PLANKA | Unterschied |
|---|---|---|---|
| Kommentar angezeigt | 302 ms | 90 ms | PLANKA 3.4× früher |
| Kommentar vom Server bestätigt | 302 ms | 229 ms | PLANKA 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 Sitzung | Unser Board | PLANKA | Unterschied |
|---|---|---|---|
| Interaction to Next Paint (INP) | 64 ms | 256 ms | 4× kürzer |
| JavaScript-Heap | 4.4 MB | 45.0 MB | 10× weniger |
| DOM-Elemente | 1’843 | 3’295 | 1.8× weniger |
| Karte öffnen | 258 ms | 318 ms | 1.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.
| Messwert | 50 Personen, jede verschiebt eine Karte pro Sekunde | 200 Personen, jede verschiebt vier Karten pro Sekunde |
|---|---|---|
| Verschobene Karten pro Sekunde | 48 | 790 |
| Als veraltet abgelehnt | 15 von 1’508 | 4’425 von 23’967 |
| Request beantwortet (204), p95 | 14 ms | 12 ms |
| Ergebnis auf dem eigenen Stream, p95 | 77 ms | 99 ms |
| Bytes pro Frame auf der Leitung | 532 | 999 |
| CPU des Servers | 1.6 Kerne | 4.9 Kerne |
| Speicher des Servers | 258 MB | 1.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 fullfsyncist 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
| Im Ruhezustand nach den Läufen | Unser Board | PLANKA |
|---|---|---|
| Was läuft | ein Binary mit 13.5 MB unter systemd | zwei Container: PLANKA (Image 379 MB) und PostgreSQL 16 (Image 294 MB) |
| Prozesse | 1 | 18 |
| Speicher | 28 MB | 200 MB |
| Dieselben Boards auf der Disk | SQLite-Datei mit 143 kB | PostgreSQL-Datenbank mit 10.3 MB |
| Abhängigkeiten | 12 Go-Module, einkompiliert | 83 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-morphtragen. - 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.




