inkseta rendering library for AI chat UIs in React
Scenario
Width480px
inkset
~50 KB
@inkset/{core,react,code,math,table,diagram}
render0.00000ms
peak0.00000ms
count0
Loading…
react-markdown
~150 KB
react-markdown@10 + gfm + math + katex + highlight
render0.00000ms
peak0.00000ms
count0

Why streaming markdown needs a renderer

Rendering markdown in an AI chat UI looks simple until you hit code blocks, math, and a user resizing their window mid-stream.

The cost of reflow

Every getBoundingClientRect() call while tokens arrive forces a synchronous layout pass.

// Naive approach — reflow per token
element.textContent = nextChunk;
const height = element.getBoundingClientRect().height;

What each renderer does

RendererStreamingPluginsReflow cost
react-markdownRe-parses per tokenWire yourselfFull DOM reflow
streamdownIncremental blocksBuilt-inReflow per block
inksetMeasured arithmeticBuilt-inO(1) arithmetic

The cost drops from:

tresizetreflow+tmeasure+tpatch+tpaintt_{resize} \approx t_{reflow} + t_{measure} + t_{patch} + t_{paint}

to:

tresizetarithmetic+tpaintt_{resize} \approx t_{arithmetic} + t_{paint}

  • One caveat: the cheap path requires a measurement cache hit.
  • On cache miss (font or content change) it's a normal measure pass.
  • Container resize is a cache hit.
  • Rich async blocks are cached too: once math or highlighted code settles at a width, revisiting that width reuses the settled height instead of replaying the shift.
streamdown
~180 KB
streamdown@2.5 + @streamdown/{code,math,mermaid}
render0.00000ms
peak0.00000ms
count0
requires tailwind — components ship as tailwind classes, so without it in the host app code blocks and controls render unstyled

Why streaming markdown needs a renderer

Rendering markdown in an AI chat UI looks simple until you hit code blocks, math, and a user resizing their window mid-stream.

The cost of reflow

Every getBoundingClientRect() call while tokens arrive forces a synchronous layout pass.

ts
// Naive approach — reflow per tokenelement.textContent = nextChunk;const height = element.getBoundingClientRect().height;

What each renderer does

RendererStreamingPluginsReflow cost
react-markdownRe-parses per tokenWire yourselfFull DOM reflow
streamdownIncremental blocksBuilt-inReflow per block
inksetMeasured arithmeticBuilt-inO(1) arithmetic

The cost drops from:

tresizetreflow+tmeasure+tpatch+tpaintt_{resize} \approx t_{reflow} + t_{measure} + t_{patch} + t_{paint}

to:

tresizetarithmetic+tpaintt_{resize} \approx t_{arithmetic} + t_{paint}

  • One caveat: the cheap path requires a measurement cache hit.
  • On cache miss (font or content change) it's a normal measure pass.
  • Container resize is a cache hit.
  • Rich async blocks are cached too: once math or highlighted code settles at a width, revisiting that width reuses the settled height instead of replaying the shift.