Steven
Steven5 min läsning

Så streamar du en live-rapport utan flimret

När ditt möte tar slut fylls GeekByes sammanfattning nu i live istället för att tvinga dig att stirra på en snurra. Att få ett streamande gränssnitt att kännas lugnt istället för ryckigt krävde att vi löste flimret två gånger — en gång för strukturerade fält, en gång för markdown — och den andra fixen var en renderare vi redan ägde.

Utveckling
Frontend
Streaming
GeekBye-releaser
Så streamar du en live-rapport utan flimret

Före den här releasen innebar det att avsluta ett GeekBye-möte att man fick titta på en snurra. Appen skickade iväg hela ditt transkript för analys, väntade på hela sammanfattningen — poäng, nyckelpunkter, åtgärdspunkter, alltihop — och släppte sedan ner allt på skärmen på en gång. Funktionellt, men det kändes långsamt, eftersom du stirrade på ingenting medan det arbetade.

GeekBye v1.6.13–v1.6.15 förvandlade det till en rapport som streamar in live, fält för fält, allteftersom den genereras. Det intressanta är inte streamingen — det är allt vi var tvungna att göra för att hindra streamingen från att se ryckig ut. Lugn streaming och skakig streaming är samma funktion med väldigt olika utförande.

Flimmer, del ett: låt inte varje chunk piska React

Sammanfattningen är inte en enda klump — det är strukturerade fält (en övergripande poäng, nyckelpunkter, åtgärdspunkter och så vidare) som backend skickar ut ett i taget över server-sent events. Det naiva sättet att rendera det är att uppdatera Reacts state vid varje event som anländer.

Gör det, och du får flimmer. Varje fält som anländer utlöser sin egen omrendering; komponenter som visade "tomt" monteras, avmonteras och remonteras sedan som "laddade"; layouten rycker till vid varje paket. Rapporten sätter ihop sig själv framför dig på värsta tänkbara sätt — synligt, nervöst.

Fixen är en disciplin i två delar. Först, ackumulera de inkommande fälten i en ref — en muterbar behållare som inte utlöser renderingar — och publicera till Reacts state en gång per chunk med en färsk kopia, så att trädet uppdateras medvetet snarare än vid varje mikro-event. För det andra, rendera ett stabilt komponentträd hela tiden, oavsett om det streamar eller är klart, med inline-platshållare för fält som inte anlänt än:

// 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>

Komponenten som visar poängen avmonteras aldrig — den visar bara tills siffran landar, sedan siffran. Ingenting rycker till. Och det fullständigt sammansatta objektet cachas ändå till den lokala databasen till slut, så att streamande gränssnitt och beständig lagring samexisterar utan att slåss.

Flimmer, del två: renderaren vi redan ägde

Det andra flimret är subtilare och bor i själva markdownen. Rapporten renderar formaterad markdown — rubriker, fetstil, listor, kodblock. Men markdown som anländer mitt i strömmen är, vid varje given ögonblick, halvfärdig. En lista med bara en punkt hittills. Ett kodblock som öppnats men inte stängts. En fetstilsmarkör utan partner än.

En konventionell markdown-renderare tolkar om hela strängen vid varje token, och det halvfärdiga tillståndet tolkas till något annat varje gång — så listan flimrar, kodblocket blinkar öppet och stängt, layouten hoppar allteftersom tokens blir färdiga. Det är samma nervösa ihopsättning som förut, ett lager längre ner.

Fixen var nästan pinsamt tillgänglig: vi använde redan en streaming-medveten markdown-renderare för live-AI-chatten. En renderare byggd för att tolerera ofullständiga tokens — för att rendera halvfärdig markdown stabilt och bara stadga den när tokens blir färdiga — istället för att tolka om från början varje gång. Rapporten behövde bara använda samma. Vi hade löst exakt det här problemet för chatten månader tidigare; "fixen" för rapporten var att inse att vi redan hade verktyget och peka det mot en andra yta. Återanvändning slog ombyggnad.

Bonusen: formaterad kopiering är ett urklippsformat-problem

När rapporten väl såg bra ut ville folk klistra in den — i ett dokument, ett mejl — och behålla formateringen och skärmbilderna. Instinkten är att serialisera rapporten till en markdown-sträng och hoppas att destinationen renderar den. Det gör den oftast inte.

Det riktiga svaret är att urklipp håller flera representationer på en gång. Så vi skriver två: en HTML-version med formateringen intakt och skärmbilderna inbäddade som inline-bilder, och en fallback i ren text för destinationer som inte tar HTML. Klistra in i en formaterad editor och du får den formaterade rapporten med bilder; klistra in i en ren textruta och du får ren text. "Formaterad kopiering" var aldrig ett serialiseringsproblem — det var ett välj-rätt-urklippsformat-problem.

Samma release gav också rapporten chattlika Me / Them-etiketter med de två talarna justerade mot motsatta sidor, så att ett transkript läses som den konversation det var, och flyttade dina egna skärmbilder till sin egen sida för att matcha. (En release i det här fönstret, v1.6.14, var en ren ombyggnad — ingen historia där, och det är ärligt att säga det.)

Tre saker streamande gränssnitt lärde oss

  1. Buffra streamade fält, publicera en gång per chunk. Att låta varje server-sent event driva sin egen state-uppdatering är flimret. En ref-ackumulator plus en enda publicering per chunk, in i ett träd som aldrig avmonteras, förvandlar nervös ihopsättning till lugn ifyllning.
  2. Allt som streamar behöver en streaming-medveten renderare. En renderare som tolkar om hela strängen per token kommer att flimra på halvfärdig markdown. Använd en byggd för ofullständig indata — och om du redan har en för en annan yta, återanvänd den innan du bygger en andra.
  3. Formaterad kopiering handlar om urklippsformat, inte serialisering. Skriv text/html med inbäddade bilder och en text/plain-fallback. Urklippet designades för att bära båda; använd det.

Det här är det femte kapitlet i tillförlitlighets- och finslipningshistorien som blir GeekBye v2. För det föregående kapitlet, se earbuds-buggen (v1.6.12); för var samma server-sent-events-idé senare blev en hel fallback-transport, live-transkribering när brandväggen blockerar WebSockets (v2.0.8); och för hela bågen, anatomin av att skeppa mjukvara till perfektion.

Relaterade artiklar