React Markdown Streaming ohne Flackern und Scroll-Sprünge

Du schickst die Antwort eines Modells durch react-markdown, und meistens klappt das. Dann fallen dir die Details auf: **$22,7 steht mit rohen Sternchen auf dem Bildschirm, bevor es fett wird. Ein Codeblock öffnet sich, und plötzlich steht der Rest der Antwort in Monospace. Eine Tabelle erscheint als Zeile voller Pipes. Und wer den ersten Absatz noch einmal lesen will, wird von der Ansicht immer wieder nach unten gezogen. React Markdown Streaming hat drei getrennte Probleme: halb getippte Syntax, das Neu-Parsen der ganzen Antwort bei jedem Chunk und das Scrollen. Dieses Tutorial löst jedes davon und setzt sie dann in einem kleinen Chat zusammen. Der Code nutzt den Markdown-Renderer, Chat Message und den Hook useStickToBottom von Wingo UI, der Bibliothek, die wir bauen. Er erklärt aber auch die Technik, damit du sie mit reinem react-markdown umsetzen kannst.
Warum flackert gestreamtes Markdown in React?
Das typische react-markdown Flackern beim Streaming hat eine einfache Ursache: Markdown ist erst wohlgeformt, wenn es fertig ist, und ein Stream zeigt dir jeden unfertigen Zustand dazwischen. Ein Modell schickt jeweils ein paar Zeichen. In jedem Moment kann das Ende des Textes ein offenes ** sein, ein Backtick ohne Partner, ein Link mit halber URL oder ein Tabellenkopf ohne Trennzeile. Der Parser macht mit dieser Eingabe genau das, was CommonMark vorschreibt. Du siehst also rohe Syntax und ein paar Tokens später die formatierte Version.
Die zweite Ursache sind die Kosten. Jeder Chunk erzeugt einen neuen Quelltext, und react-markdown lässt remark und rehype über alles laufen, was bisher angekommen ist. Die Arbeit pro Chunk wächst mit der Antwort, die Gesamtarbeit also mit dem Quadrat ihrer Länge. Das frühere Memoisierungs-Rezept im AI SDK Cookbook (opens in a new tab) hat geparste Blöcke memoisiert, damit sie nicht bei jedem Token-Update erneut geparst und gerendert werden. React gleicht die Ausgabe weiterhin per Reconciliation ab, deshalb übersteht der Großteil des DOM ein Re-Render. Die Ausnahme ist ein inline geschriebenes components-Objekt: Jeder Render übergibt React neue Komponentenfunktionen, React hält sie für neue Typen und mountet diese Elemente neu. Eine Tabelle, die jemand seitlich gescrollt hat, springt dann zurück auf ihre erste Spalte.
Die dritte Ursache ist das Scrollen, mehr dazu weiter unten. Ein Setup für Streaming Markdown in React, das nur eins der drei Probleme löst, fühlt sich trotzdem kaputt an. Deshalb behandelt dieses Tutorial alle drei.
Wie reparierst du halb getipptes Markdown vor dem Parsen?
Schreib nur das unfertige Ende des Quelltexts um, bevor der Parser es sieht. Dann ist jeder Zwischenzustand gültiges Markdown, das wie das fertige aussieht. Die Markdown-Komponente exportiert ihren Reparaturschritt als reine Funktion, completeMarkdown(text). Das gibt sie für echte Eingaben mitten im Stream zurück:
Zwei Regeln sorgen dafür, dass die Reparatur nichts verfälscht. Sie fasst nichts innerhalb eines Codeblocks an, ein ** in einem Codebeispiel bleibt also wörtlich. Und sie fügt nur schließende Zeichen hinzu oder hält Text zurück; sie erfindet nie Inhalt. Kommt das echte schließende Zeichen an, sieht die Formatierung genauso aus wie vorher. Der Text springt also nicht zwischen roh und formatiert hin und her.
Bleibst du bei reinem react-markdown, passt dieselbe Idee in eine Zeile, solange die Antwort streamt: <ReactMarkdown>{streaming ? completeMarkdown(text) : text}</ReactMarkdown>. Die Funktion steckt in components/ui/markdown.tsx, nimmt einen String, gibt einen String zurück und braucht kein React.
Wie verhinderst du, dass bei jedem Token die ganze Antwort neu geparst wird?
Teil den Quelltext an Leerzeilen in Blöcke der obersten Ebene, render jeden Block in einer eigenen memoisierten Komponente und vergib die Keys nach Position. Während der Text streamt, ändert sich nur der String des letzten Blocks, also wird nur der letzte Block neu geparst. Jeder fertige Absatz, jede Tabelle und jeder Codeblock behält sein DOM.
Die Aufteilung muss die Struktur von Markdown respektieren. Eine Leerzeile in einem Code-Fence beendet den Block nicht, und eine Leerzeile zwischen den Punkten einer lockeren Liste auch nicht. splitMarkdownBlocks aus derselben Datei kann beides:
Das frühere AI-SDK-Rezept hat mit marked.lexer aufgeteilt und den content jedes Blocks in React.memo verglichen. Das löst das Kostenproblem, aber Memoisierung allein lässt das Flackern im letzten Block stehen, und genau den hat man gerade vor Augen. Lass also zuerst die Reparatur laufen und teil danach auf.
Zwei Einschränkungen. Referenzlinks und Fußnoten ([id]: https://...) werden über das ganze Dokument aufgelöst, ein blockweises Rendern kann sie also nicht auflösen. Solange Text streamt, erscheint ein [docs][1], dessen Definition in einem anderen Block steht, als wörtliche Klammern. Sobald streaming ausgeht, rendert die Markdown-Komponente das ganze Dokument in einem Durchgang, wenn der Quelltext eine Definition enthält (oder wenn du eigene remark- oder rehype-Plugins übergeben hast), und die Referenzen lösen sich auf. Halte außerdem jedes components-Override stabil (auf Modulebene oder mit useMemo). Ein Inline-Objekt hebelt das Memo aus, dann rendert jeder Block bei jedem Chunk neu, und Inline-Funktionen mounten ihre Elemente neu, wie oben beschrieben.
Drück unten auf „Replay as a stream“ und achte auf die fette Summe und den Tabellenkopf. Sie bleiben verborgen, bis sie vollständig sind, statt rohe Zeichen aufblitzen zu lassen, und jeder neue Block blendet sich unter einem pulsierenden Caret ein.
$ npx wingo-ui@latest add markdownWie renderst du eine gestreamte Antwort mit der Markdown-Komponente?
Installier sie, häng jeden Chunk an den State an und übergib streaming, solange die Antwort ankommt:
/api/chat ist dein Route Handler, der einen reinen Text-Stream zurückgibt. Ist streaming an, repariert die Komponente das Ende, parst nur den letzten Block neu, blendet neue Blöcke ein, zeigt den Caret hinter dem letzten Wort und setzt aria-busy auf ihr Root-Element. Der Caret ist standardmäßig dot; bar, block und none sind die anderen Optionen. Schalte streaming aus, wenn der Stream endet: Die Reparatur hört auf, der Caret verschwindet, und die fertigen Blöcke behalten ihr DOM.
Bei Modell-Output kommt es auf zwei Einstellungen an. variant="compact" hält Überschriften klein, damit eine Antwort, die mit # beginnt, nicht schreit. allowImages={false} rendert jedes Bild als Link mit seinem Alt-Text, damit Output, dem du nicht trauen kannst, kein Tracking-Pixel lädt. Rohes HTML im Quelltext wird nie gerendert.
Wie hältst du einen Chat beim Streamen unten?
Folge neuem Inhalt nur, solange die Person unten ist, hör sofort auf, wenn sie nach oben scrollt, und setz wieder ein, wenn sie zurückscrollt oder eine Nachricht schickt. Der erste Anlauf für einen Chat Auto Scroll nach unten in React ist oft ein useEffect, der scrollt, sobald sich das Messages-Array ändert. Beim Streamen scheitert das in beide Richtungen: Es verpasst Höhe, die sich ohne State-Update ändert, und es zieht jeden zurück, der nach oben scrollt. Vier Regeln sorgen dafür, dass sich ein Stick to Bottom Chat richtig anfühlt:
- Schon bei der Absicht loslassen. Ganz unten hat eine Mausrad-Drehung nach oben
scrollTopnoch nicht bewegt, wenn der nächste Chunk ankommt. Eine reine Positionsprüfung scrollt die Person also direkt zurück. Der Hook hört deshalb schon beim Wheel-Event nach oben auf zu folgen, außerdem bei jedem Scrollen nach oben, solange der Inhalt nicht geschrumpft ist, auch innerhalb der Schwelle. - Die Größe des Inhalts beobachten. Die Höhe ändert sich auch ohne State-Update: Ein Bild lädt fertig, ein Reasoning-Panel klappt auf. Ein
ResizeObserverauf dem wachsenden Element erfasst jede Änderung, egal woher sie kommt, und einMutationObserverbemerkt ein Kind, das später eingesetzt wird, etwa einen Empty State, den die Liste ersetzt. - Außerhalb von React folgen. Das Folgen schreibt
scrollTopin einerrequestAnimationFrame-Schleife, die das Ziel in jedem Frame neu liest. So wird auch Text erfasst, der mitten in der Animation wächst. React-State ändert sich nur, wennisAtBottomoderisFollowingumspringt. - Reduzierte Bewegung respektieren. Mit reduzierter Bewegung springt das Folgen, statt weich zu gleiten.
Vielleicht liest du den Tipp, dafür CSS Scroll Anchoring zu nehmen. Es wurde dafür entwickelt, sichtbaren Inhalt ruhig zu halten, wenn sich darüber etwas ändert. Um damit unten zu bleiben, brauchst du einen Trick mit einem Anker-Element, und es liefert dir keinen isAtBottom-Status für einen Sprung-Button. Die MDN-Seite zu overflow-anchor (opens in a new tab) führt es erst seit September 2026 als Baseline (Stand Oktober 2026), also fehlt es Smartphones mit älteren Browserversionen noch.
scrollRef kommt auf das Element mit overflow-y-auto, contentRef auf das Element darin, das wächst. Scroll nach oben, während die Antwort darunter noch ankommt: Das Folgen hört sofort auf, der Text wächst unten weiter, und ein „New messages“-Button erscheint.
$ npx wingo-ui@latest add use-stick-to-bottomWie baust du einen Streaming-Markdown-Chat in React?
Render jede Nachricht mit Chat Message in einem Scroller mit useStickToBottom und ersetz bei jedem Chunk nur das Objekt der Nachricht, die gerade streamt. Chat Message rendert content mit derselben Markdown-Komponente und schaltet Streaming ein, wenn status gleich "streaming" ist. Die Komponente steckt in memo, die fertigen Nachrichten darüber behalten also ihre Props und rendern nicht neu. Hier ist alles in einer Datei:
Das machen die einzelnen Teile, während eine Antwort streamt: Die wartende Nachricht zeigt Tipp-Punkte bis zum ersten Chunk. Die streamende Nachricht repariert ihr Markdown und parst nur ihren letzten Block neu. Die anderen Nachrichten überspringen das Rendern. Und der Scroller folgt der wachsenden Antwort, bis die Person nach oben scrollt. Ein abgebrochener Request behält den Text, der angekommen ist, und markiert die Nachricht als gestoppt.
Diese Datei lässt weg, was ein Thread in Produktion zusätzlich braucht: einen Zähler neuer Antworten auf dem Sprung-Button, eine stabile Leseposition, wenn oben ältere Nachrichten nachladen, und eine Live Region im Modus „polite“, die ansagt, wann eine Antwort beginnt und wann sie fertig ist, statt jedes gestreamte Wort vorzulesen. Das übernimmt die Komponente Chat Thread. In ihren Composer-Slot kommt meist ein Prompt Input. Der ignoriert Enter, solange eine IME-Eingabe noch läuft, und bleibt mit position="fixed" über der Smartphone-Tastatur. Der Guide zur React AI Chat UI geht jedes Teil durch.
Wann nimmst du lieber Streamdown oder use-stick-to-bottom?
Wenn du kostenlose Open-Source-Bausteine willst und sie gern selbst zusammensetzt. Beide sind eine gute Wahl, Stand Oktober 2026:
- Streamdown (opens in a new tab) von Vercel steht unter Apache-2.0 und beschreibt sich als Drop-in-Ersatz für react-markdown, gebaut für KI-Streaming, mit memoisiertem Rendering. Laut seiner Doku zur Termination (opens in a new tab) schließt der Präprozessor remend unvollständigen Fettdruck, Kursivschrift, Inline-Code, Durchstreichung, Links und Mathe-Blöcke. Enthalten deine Antworten Mathe, nimm Streamdown: Unsere Markdown-Komponente rendert GitHub Flavored Markdown ohne Mathe-Unterstützung. Nutzt deine App schon das AI SDK, ist Streamdown ebenfalls die naheliegende Wahl.
- use-stick-to-bottom (opens in a new tab) von StackBlitz Labs ist ein Hook samt Komponente unter MIT-Lizenz, ohne Abhängigkeiten, mit geschwindigkeitsbasiertem Spring-Scrollen. Wer nach oben scrollt, beendet das Mitscrollen. Unser Hook teilt seinen Namen und sein Ziel.
Die Versionen von Wingo UI passen, wenn Renderer, Nachricht und Scroller einen Look und einen Satz Props teilen sollen und das Verhalten auf Smartphones schon erledigt ist: Text bleibt im Chat bei 15px, breite Tabellen und Code scrollen in ihrer eigenen Box, und langes Drücken auf eine Nachricht öffnet auf Touchscreens ihre Aktionen in einem Bottom Sheet. Markdown, Chat Message und useStickToBottom gehören zu Wingo UI Pro (Preise).
Was probierst du als Nächstes?
Öffne die Markdown-Doku, drück im Playground auf „Replay as a stream“ und installier die Komponente dann mit npx wingo-ui@latest add markdown. Baust du mit einem Coding-Agenten, zeigt der Guide zum MCP für Komponentenbibliotheken, wie er diese Komponenten direkt installieren kann. Mehr Beiträge wie diesen findest du unter UI mit KI-Agenten bauen.
Häufige Fragen
Warum flackert react-markdown, während eine KI-Antwort streamt?
Jeder Chunk ändert den Quelltext, also parst und rendert react-markdown die ganze Antwort neu, und unfertige Syntax wie ein offenes ** oder ein halb getippter Link erscheint als rohe Zeichen, bis das Gegenstück ankommt. Der Text springt dann alle paar Tokens zwischen roh und formatiert hin und her.
Wie verhindere ich, dass ein gestreamter Codeblock den Rest der Nachricht kaputt macht?
Schließ den offenen Fence vor dem Parsen selbst: Hat der Text ein öffnendes ``` ohne Gegenstück, häng eins an. Halte den Fence außerdem zurück, bis seine erste Zeile endet, sonst kann ein halb getippter Sprachname kurz das falsche Highlighting auswählen.
Reicht es, Markdown-Blöcke beim Streaming zu memoisieren?
Es löst das Kostenproblem, weil fertige Blöcke nicht mehr neu geparst werden, aber der letzte Block zeigt weiter halb getippten Fettdruck, Links und Tabellenköpfe. Du brauchst beides: Block-Memoisierung für die Geschwindigkeit und einen Reparaturschritt am Ende gegen das Flackern.
Wie scrollt ein React-Chat automatisch nach unten, aber nur, wenn die Person schon unten ist?
Prüf, ob der Scroller innerhalb einer kleinen Schwelle vor seinem Ende steht, folge neuem Inhalt nur, solange das stimmt, und hör sofort auf, sobald die Person nach oben scrollt, auch nur um ein paar Pixel. Ein ResizeObserver auf dem Inhalt erfasst jede Höhenänderung, auch solche ohne State-Update, etwa ein Bild, das fertig lädt.
Reicht CSS overflow-anchor allein für Stick to Bottom?
Fürs Streaming nicht zuverlässig. Scroll Anchoring soll sichtbaren Inhalt ruhig halten, wenn sich darüber etwas ändert, es liefert keinen At-Bottom-Status für einen Sprung-Button, und MDN führt overflow-anchor erst seit September 2026 als Baseline, also fehlt es älteren Browsern auf Smartphones noch.
- Markdown
- Streaming
- KI-Chat
- React
- Scrollen


