
Hogyan streamelj élő jelentést villódzás nélkül
Amikor véget ér a megbeszélésed, a GeekBye összefoglalója mostantól élőben töltődik ki, ahelyett hogy egy töltésjelzőt kellene bámulnod. Ahhoz, hogy a streamelő felület nyugodtnak tűnjön, ne pedig szaggatottnak, kétszer kellett megoldanunk a villódzást — egyszer a strukturált mezőkre, egyszer a markdownra —, és a második javítás egy olyan renderelő volt, amelyik már a miénk volt.
E kiadás előtt egy GeekBye-megbeszélés befejezése azt jelentette, hogy egy töltésjelzőt bámultál. Az alkalmazás elküldte az egész átiratodat elemzésre, megvárta a teljes összefoglalót — pontszám, kulcspontok, teendők, az egész csomag —, majd egyszerre borította a képernyőre. Működött, de lassúnak érződött, mert a semmit bámultad, amíg dolgozott.
A GeekBye v1.6.13–v1.6.15 ezt olyan jelentéssé alakította, amely élőben, mezőnként töltődik be, ahogy generálódik. Az érdekes rész nem a streamelés — hanem mindaz, amit meg kellett tennünk, hogy a streamelés ne tűnjön szaggatottnak. A nyugodt streamelés és az akadozó streamelés ugyanaz a funkció, csak nagyon eltérő kivitelezéssel.
Villódzás, első rész: ne hagyd, hogy minden chunk megrázza a Reactet
Az összefoglaló nem egyetlen tömb — strukturált mezők (egy összesített pontszám, kulcspontok, teendők és így tovább), amelyeket a backend egyesével bocsát ki server-sent events formájában. A naiv módszer ennek renderelésére az, hogy minden beérkező eseménynél frissíted a React állapotát.
Tedd meg ezt, és villódzást kapsz. Minden beérkező mező kiváltja a saját újrarenderelését; a „üres"-t mutató komponensek felszerelődnek, majd leszerelődnek, majd „betöltöttként" újraszerelődnek; az elrendezés minden csomagnál megrándul. A jelentés összeáll magától előtted a lehető legrosszabb módon — láthatóan, idegesen.
A javítás egy kétrészes fegyelem. Először, halmozd fel a beérkező mezőket egy refben — egy változtatható tárolóban, amely nem vált ki renderelést —, és publikálj a React állapotába chunkonként egyszer egy friss másolattal, hogy a fa szándékosan frissüljön, ne pedig minden mikroeseménynél. Másodszor, renderelj egyetlen stabil komponensfát végig, akár streamel, akár kész, inline helykitöltőkkel azokhoz a mezőkhöz, amelyek még nem érkeztek meg:
// 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>
A pontszámot mutató komponens soha nem szerelődik le — csak …-t mutat, amíg a szám meg nem érkezik, aztán a számot. Semmi nem rándul. És a teljesen összeállított objektum a végén így is a helyi adatbázisba kerül gyorsítótárba, így a streamelő felület és a tartós tárolás verekedés nélkül él együtt.
Villódzás, második rész: a renderelő, amelyik már a miénk volt
A második villódzás finomabb, és magában a markdownban él. A jelentés formázott markdownt renderel — címsorokat, félkövért, listákat, kódblokkokat. De a streamelés közben érkező markdown bármely adott pillanatban félbetört. Egy lista eddig egyetlen ponttal. Egy kódblokk, amit megnyitottak, de nem zártak le. Egy félkövér jelölő, amelynek még nincs párja.
Egy hagyományos markdown-renderelő minden tokennél újraelemzi az egész karakterláncot, és ez a félbetört állapot minden alkalommal máshogy elemződik — így a lista villódzik, a kódblokk ki-be villan, az elrendezés ugrál, ahogy a tokenek elkészülnek. Ugyanaz az ideges összeállás, mint korábban, egy réteggel lejjebb.
A javítás szinte kínosan kéznél volt: már használtunk egy streamelés-tudatos markdown-renderelőt az élő AI-chathez. Egy renderelőt, amelyet a hiányos tokenek tolerálására építettek — hogy stabilan renderelje a félbetört markdownt, és csak akkor rögzítse, amikor a tokenek elkészülnek — ahelyett, hogy minden alkalommal nulláról újraelemezne. A jelentésnek csak ugyanazt kellett használnia. Ezt a pontos problémát hónapokkal korábban megoldottuk a chathez; a jelentés „javítása" az volt, hogy felismertük, már megvan az eszközünk, és rámutattunk egy második felületre. Az újrafelhasználás legyőzte az újraépítést.
A bónusz: a formázott másolás vágólapformátum-probléma
Miután a jelentés jól nézett ki, az emberek be akarták illeszteni — egy dokumentumba, egy e-mailbe —, és megtartani a formázást meg a képernyőképeket. Az ösztön az, hogy a jelentést markdown karakterlánccá szerializálod, és reménykedsz, hogy a célhely rendereli. Általában nem teszi.
Az igazi válasz az, hogy a vágólap egyszerre több reprezentációt is tart. Ezért kettőt írunk ki: egy HTML verziót érintetlen formázással és a képernyőképekkel inline képként beágyazva, és egy egyszerű szöveges tartalékot azokhoz a célhelyekhez, amelyek nem fogadnak HTML-t. Illeszd be egy formázott szerkesztőbe, és megkapod a formázott jelentést képekkel; illeszd be egy egyszerű szövegdobozba, és tiszta szöveget kapsz. A „formázott másolás" sosem szerializációs probléma volt — hanem egy a-megfelelő-vágólapformátum-kiválasztása probléma.
Ugyanez a kiadás chat-stílusú Én / Ők címkéket is adott a jelentésnek, a két beszélővel ellentétes oldalakra igazítva, hogy egy átirat úgy olvasódjon, mint a beszélgetés, ami volt, és a saját képernyőképeidet a saját oldalukra tette, hogy passzoljanak. (Egy kiadás ebben az ablakban, a v1.6.14, tiszta újraépítés volt — ott nincs történet, és tisztességes ezt kimondani.)
Három dolog, amire a streamelő felület megtanított minket
- Puffereld a beérkező mezőket, publikálj chunkonként egyszer. Ha minden server-sent event a saját állapotfrissítését hajtja, az a villódzás. Egy ref-akkumulátor plusz egyetlen publikálás chunkonként, egy soha le nem szerelődő fába, az ideges összeállást nyugodt kitöltéssé alakítja.
- Bármihez, ami streamel, streamelés-tudatos renderelő kell. Egy renderelő, amely tokenenként újraelemzi az egész karakterláncot, villódzni fog a félbetört markdownon. Használj olyat, amelyet hiányos bemenetre építettek — és ha már van egy másik felülethez, használd újra, mielőtt másodikat építenél.
- A formázott másolás vágólapformátumokról szól, nem szerializációról. Írj
text/html-t beágyazott képekkel és egytext/plaintartalékot. A vágólapot úgy tervezték, hogy mindkettőt hordozza; használd ki.
Ez a megbízhatóság-és-csiszolás történet ötödik fejezete, amely GeekBye v2 lesz. Az előző fejezethez lásd a fülhallgatós hiba (v1.6.12); ahhoz, ahol ugyanaz a server-sent-events ötlet később egy egész tartalék átvitellé vált, élő átírás, amikor a tűzfal blokkolja a WebSocketeket (v2.0.8); a teljes ívhez pedig a szoftver tökéletességig szállításának anatómiája.