
Wie man einen Live-Bericht ohne das Flackern streamt
Wenn dein Meeting endet, füllt sich die Zusammenfassung von GeekBye jetzt live auf, statt dich auf einen Spinner starren zu lassen. Eine Streaming-Oberfläche ruhig statt zappelig wirken zu lassen, erforderte, das Flackern zweimal zu lösen — einmal für strukturierte Felder, einmal für Markdown — und die zweite Lösung war ein Renderer, den wir bereits besaßen.
Vor diesem Release bedeutete das Beenden eines GeekBye-Meetings, auf einen Spinner zu starren. Die App schickte dein gesamtes Transkript zur Analyse, wartete auf die komplette Zusammenfassung — Bewertung, Kernpunkte, Aufgaben, alles — und ließ sie dann auf einen Schlag auf den Bildschirm fallen. Funktional, aber es fühlte sich langsam an, weil du ins Leere starrtest, während sie arbeitete.
GeekBye v1.6.13–v1.6.15 verwandelte das in einen Bericht, der live hereinstreamt, Feld für Feld, während er generiert wird. Der interessante Teil ist nicht das Streaming — es ist alles, was wir tun mussten, um das Streaming davon abzuhalten, zappelig auszusehen. Ruhiges Streaming und ruckeliges Streaming sind dieselbe Funktion mit sehr unterschiedlicher Umsetzung.
Flackern, Teil eins: lass nicht jeden Chunk React durchschütteln
Die Zusammenfassung ist kein einzelner Klumpen — es sind strukturierte Felder (eine Gesamtbewertung, Kernpunkte, Aufgaben und so weiter), die das Backend eines nach dem anderen über server-sent events ausgibt. Der naive Weg, das zu rendern, ist, den React-State bei jedem Ereignis zu aktualisieren, sobald es eintrifft.
Tu das, und du bekommst Flackern. Jedes eintreffende Feld löst sein eigenes Neu-Rendern aus; Komponenten, die „leer" zeigten, werden montiert, dann ausgehängt, dann als „geladen" neu montiert; das Layout zuckt bei jedem Paket. Der Bericht setzt sich vor deinen Augen selbst zusammen — auf die denkbar schlechteste Art, sichtbar, nervös.
Die Lösung ist eine zweiteilige Disziplin. Erstens: sammle die eintreffenden Felder in einem ref — einem veränderlichen Halter, der keine Renders auslöst — und veröffentliche einmal pro Chunk mit einer frischen Kopie in den React-State, damit sich der Baum bewusst aktualisiert statt bei jedem Mikro-Ereignis. Zweitens: rendere die ganze Zeit einen einzigen stabilen Komponentenbaum, ob im Streaming oder fertig, mit Inline-Platzhaltern für Felder, die noch nicht eingetroffen sind:
// accumulate in a ref, publish once per chunk
partialRef.current = { ...partialRef.current, [field]: value }
setPartial({ ...partialRef.current })
// one tree, never swapped; missing fields show a placeholder in place
const report = isStreaming ? partial : saved
<Score>{report.overallScore ?? '…'}</Score>
Die Komponente, die die Bewertung anzeigt, wird nie ausgehängt — sie zeigt einfach …, bis die Zahl landet, dann die Zahl. Nichts zuckt. Und das vollständig zusammengesetzte Objekt wird am Ende trotzdem in der lokalen Datenbank zwischengespeichert, sodass Streaming-Oberfläche und dauerhafte Speicherung koexistieren, ohne sich zu bekämpfen.
Flackern, Teil zwei: der Renderer, den wir bereits besaßen
Das zweite Flackern ist subtiler und lebt im Markdown selbst. Der Bericht rendert formatiertes Markdown — Überschriften, Fettdruck, Listen, Code-Blöcke. Aber Markdown, das mitten im Stream eintrifft, ist in jedem gegebenen Moment halbfertig. Eine Liste mit bisher einem Aufzählungspunkt. Ein Code-Zaun, der geöffnet, aber nicht geschlossen wurde. Ein Fett-Marker noch ohne Partner.
Ein herkömmlicher Markdown-Renderer parst bei jedem Token die gesamte Zeichenkette neu, und dieser halbfertige Zustand parst jedes Mal zu etwas anderem — also flackert die Liste, der Code-Block blitzt auf und zu, das Layout springt, während die Tokens fertig werden. Es ist dieselbe nervöse Montage wie zuvor, eine Ebene tiefer.
Die Lösung war fast peinlich verfügbar: wir nutzten bereits einen streaming-bewussten Markdown-Renderer für den KI-Live-Chat. Ein Renderer, gebaut, um unvollständige Tokens zu vertragen — halbfertiges Markdown stabil zu rendern und es erst festzulegen, wenn die Tokens fertig sind — statt jedes Mal von Grund auf neu zu parsen. Der Bericht musste nur denselben verwenden. Wir hatten genau dieses Problem Monate zuvor für den Chat gelöst; die „Lösung" für den Bericht war zu erkennen, dass wir das Werkzeug bereits hatten, und es auf eine zweite Oberfläche zu richten. Wiederverwenden schlug Neubauen.
Der Bonus: formatiertes Kopieren ist ein Zwischenablage-Format-Problem
Sobald der Bericht gut aussah, wollten die Leute ihn einfügen — in ein Dokument, eine E-Mail — und die Formatierung und die Screenshots behalten. Der Instinkt ist, den Bericht in eine Markdown-Zeichenkette zu serialisieren und zu hoffen, dass das Ziel sie rendert. Meistens tut es das nicht.
Die wahre Antwort ist, dass die Zwischenablage mehrere Darstellungen gleichzeitig hält. Also schreiben wir zwei: eine HTML-Version mit intakter Formatierung und den als Inline-Bilder eingebetteten Screenshots und einen Nur-Text-Fallback für Ziele, die kein HTML annehmen. Füge in einen Rich-Editor ein und du bekommst den formatierten Bericht mit Bildern; füge in ein Nur-Text-Feld ein und du bekommst sauberen Text. „Formatiertes Kopieren" war nie ein Serialisierungsproblem — es war ein Problem des Das-richtige-Zwischenablage-Format-Wählens.
Dasselbe Release gab dem Bericht außerdem Chat-artige Ich / Sie-Beschriftungen mit den beiden Sprechern an gegenüberliegenden Seiten ausgerichtet, sodass sich ein Transkript wie das Gespräch liest, das es war, und verschob deine eigenen Screenshots passend auf ihre eigene Seite. (Ein Release in diesem Fenster, v1.6.14, war ein reiner Neubau — keine Geschichte darin, und es ist ehrlich, das zu sagen.)
Drei Dinge, die uns die Streaming-Oberfläche lehrte
- Puffere gestreamte Felder, veröffentliche einmal pro Chunk. Jedes server-sent event seine eigene State-Aktualisierung antreiben zu lassen, ist das Flackern. Ein ref-Akkumulator plus eine einzige Veröffentlichung pro Chunk, in einen Baum, der nie ausgehängt wird, verwandelt nervöse Montage in ruhiges Auffüllen.
- Alles, was streamt, braucht einen streaming-bewussten Renderer. Ein Renderer, der pro Token die gesamte Zeichenkette neu parst, flackert bei halbfertigem Markdown. Nutze einen, der für unvollständige Eingaben gebaut ist — und wenn du schon einen für eine andere Oberfläche hast, verwende ihn wieder, bevor du einen zweiten baust.
- Formatiertes Kopieren dreht sich um Zwischenablage-Formate, nicht um Serialisierung. Schreibe
text/htmlmit eingebetteten Bildern und einentext/plain-Fallback. Die Zwischenablage wurde entworfen, um beides zu tragen; nutze sie.
Dies ist das fünfte Kapitel der Zuverlässigkeits- und Feinschliff-Geschichte, die zu GeekBye v2 wird. Für das vorherige Kapitel siehe den Ohrhörer-Bug (v1.6.12); für die Stelle, an der dieselbe server-sent-events-Idee später zu einem ganzen Fallback-Transport wurde, Live-Transkription, wenn die Firewall WebSockets blockiert (v2.0.8); und für den gesamten Bogen die Anatomie, Software bis zur Perfektion auszuliefern.