Streaming
The same answer, cut into the same tokens, streamed into five renderers at once. It starts when the
panels scroll into view. Watch for three things: a half marker (**bo, an opening backtick)
showing up as text and then vanishing, bold that ends right before a Korean particle
(**설정(config)**을) never getting drawn, and emphasis or a table that freezes until it closes.
Loading the WASM build…
Loading the WASM build…
- react-markdown redraws the whole accumulated text on every token, so half-written markup flickers in and out.
- Streamdown redraws too, but repairs unclosed syntax first.
- mdwire → Streamdown puts mdwire’s streamer in front: Streamdown receives the normalized output
plus
preview()— the bridge. - mdwire
useMarkdownStream()draws what is still held (open bold, table rows) early, as if the input ended here. - mdwire
useMarkdownStream({ eager: false })shows only what is final. Nothing it shows ever changes, at the cost of pausing on emphasis and tables.
DESIGN.md measured this on 347 Korean documents written by agents (Streamdown 2.6.0,
react-markdown 10.1.0 + remark-gfm): putting mdwire’s streamer in front of Streamdown cut changes to
already-visible text by about 60% and stopped markers from showing as text, and mdwire’s own html
streamer, showing only what is final, had neither. Showing only what is final pauses, though: with
this sample at a 60 ms token interval the longest pause was 1,389 ms against 302 ms with the
preview — which is why the preview is the default. See the
bridge section of DESIGN.md (Korean).
The sample answer is synthetic, but each of its shapes broke in real agent output.
This demo’s code
import { Streamer } from "@minjun0219/mdwire";
import { useMarkdownStream } from "@minjun0219/mdwire/react";
import { Streamdown } from "streamdown";
// the bridge — normalize with mdwire, draw with Streamdown
const s = new Streamer("github-markdown");
let done = "";
for (const token of tokens) {
done += s.push(token); // final; never rewritten
setText(done + s.preview()); // + what is still held, drawn as if the input ended here
}
done += s.finish();
setText(done);
// <Streamdown>{text}</Streamdown>
// or draw straight from React, without innerHTML
const live = useMarkdownStream(); // draws held content early (default)
const settled = useMarkdownStream({ eager: false }); // only what is final
// tokens → live.push(token) … live.finish(); render {live.elements}Full component: site/src/components/StreamCompare.tsx. The same comparison as a standalone Vite app, with the bundler settings for loading the WASM: examples/react-streaming.