Summary
In streamdown@2.5.0, a <Streamdown> in the default streaming mode without an animated config pushes its parsed-block state updates through useTransition. Those low-priority updates can be starved indefinitely by sibling urgent updates — e.g. another component ticking state at animation cadence — so the rendered markdown freezes at its first parse and never updates again until the urgent-update pressure stops, even though children keeps growing and the component keeps re-rendering.
The practical hit: a chat UI that renders a model's reasoning/thinking stream in one plain <Streamdown> while the reply streams in another (animated, or otherwise driving frequent state updates). The thinking panel freezes at its first delta — often a single character — for the whole turn, then snaps to the full text when streaming ends. It looks exactly like a data bug, and we spent a while exonerating our wire/persistence before finding it here.
Where in the source
In the main component (paraphrasing the 2.5.0 bundle):
const blocks = useMemo(() => parseMarkdownIntoBlocksFn(processedChildren), [...]);
const [deferredBlocks, setDeferredBlocks] = useState(blocks);
useEffect(() => {
mode === "streaming" && !animatePlugin
? startTransition(() => { setDeferredBlocks(blocks); }) // <-- starvable
: setDeferredBlocks(blocks);
}, [blocks, mode]);
const rendered = mode === "streaming" ? deferredBlocks : blocks;
With animated set, updates take the direct setDeferredBlocks(blocks) branch and everything streams fine — which is also the workaround.
Repro sketch
function Repro() {
const [reasoning, setReasoning] = useState(""); // grows ~10x/sec
const [, tick] = useState(0); // sibling urgent updates, 40ms
useEffect(() => {
const a = setInterval(() => setReasoning((t) => t + " lorem"), 100);
const b = setInterval(() => tick((n) => n + 1), 40);
return () => { clearInterval(a); clearInterval(b); };
}, []);
return <Streamdown>{reasoning}</Streamdown>; // freezes at the first parse while `b` runs
}
Observed in a real app (Next 16.2.6 / React 19, streamdown@2.5.0): instrumented the element with data-len={text.length} — the prop grew 22 → 139 chars and the attribute updated live (so the component re-rendered), while the rendered markdown text stayed frozen at 3 chars until the sibling stream ended. Passing animated={{ animation: "fadeIn", duration: 150, sep: "word" }} + isAnimating fixed it instantly, no other change.
Expected
Either:
- streaming-mode block updates shouldn't be starvable (e.g. fall back to a direct update if a transition hasn't committed within some budget, or don't route through
useTransition at all when the update is just "the tail block grew"), or
- the behavior/trade-off (and the
animated escape hatch) documented — right now un-animated streaming markdown next to any busy sibling silently freezes, which reads as data corruption rather than scheduling.
Happy to provide a full repro repo if useful.
Summary
In
streamdown@2.5.0, a<Streamdown>in the default streaming mode without ananimatedconfig pushes its parsed-block state updates throughuseTransition. Those low-priority updates can be starved indefinitely by sibling urgent updates — e.g. another component ticking state at animation cadence — so the rendered markdown freezes at its first parse and never updates again until the urgent-update pressure stops, even thoughchildrenkeeps growing and the component keeps re-rendering.The practical hit: a chat UI that renders a model's reasoning/thinking stream in one plain
<Streamdown>while the reply streams in another (animated, or otherwise driving frequent state updates). The thinking panel freezes at its first delta — often a single character — for the whole turn, then snaps to the full text when streaming ends. It looks exactly like a data bug, and we spent a while exonerating our wire/persistence before finding it here.Where in the source
In the main component (paraphrasing the 2.5.0 bundle):
With
animatedset, updates take the directsetDeferredBlocks(blocks)branch and everything streams fine — which is also the workaround.Repro sketch
Observed in a real app (Next 16.2.6 / React 19,
streamdown@2.5.0): instrumented the element withdata-len={text.length}— the prop grew 22 → 139 chars and the attribute updated live (so the component re-rendered), while the rendered markdown text stayed frozen at 3 chars until the sibling stream ended. Passinganimated={{ animation: "fadeIn", duration: 150, sep: "word" }}+isAnimatingfixed it instantly, no other change.Expected
Either:
useTransitionat all when the update is just "the tail block grew"), oranimatedescape hatch) documented — right now un-animated streaming markdown next to any busy sibling silently freezes, which reads as data corruption rather than scheduling.Happy to provide a full repro repo if useful.