Skip to main content
Streamdown is designed to handle large, rapidly-updating markdown documents without degrading render performance. It achieves this through block-level memoization, stable keys, and React Transitions.

Block-level memoization

In streaming mode, Streamdown splits the markdown into top-level blocks (paragraphs, headings, code fences, etc.) and renders each as a separate Block component wrapped in React.memo. Because blocks early in the document are complete and never change, React.memo prevents them from re-rendering when only the last (in-progress) block changes. In a long streaming response, this means the cost of each token arrival is roughly constant — only the last block is re-processed.

The Block component’s comparison function

Block uses a custom comparison function instead of the default shallow equality check. It re-renders only when any of these change:
  • content — the block’s markdown string
  • index — the block’s position in the array
  • isIncomplete — whether the block is still being streamed
  • dir — resolved text direction
  • shouldNormalizeHtmlIndentation — HTML normalization setting
  • components — checked with key-level shallow equality (different reference is OK as long as the component values match)
  • rehypePlugins — reference equality
  • remarkPlugins — reference equality
This avoids spurious re-renders from inline object literals while still catching meaningful prop changes.

Stable block keys

Block keys are index-based (${generatedId}-${index}), not content-hash-based. This is intentional:
  • Content-hash keys would cause every block to unmount and remount when its content changes
  • Unmounting destroys DOM state (scroll position, textarea contents, etc.) and triggers expensive remounts
  • Index-based keys allow React to reconcile the existing DOM node and apply the content update as a prop change, which React.memo then gates
// Keys generated internally — stable across re-renders
const blockKeys = blocksToRender.map((_block, idx) => `${generatedId}-${idx}`);

React Transitions for streaming updates

In streaming mode (without the animated prop), block state updates are wrapped with startTransition:
startTransition(() => {
  setDisplayBlocks(blocks);
});
This marks block updates as non-urgent. React can interrupt them to service higher-priority work like user input events (clicks, keystrokes). The result is a UI that stays responsive even when tokens arrive rapidly. When the animated prop is active, updates bypass the transition and apply synchronously. The animation plugin needs to measure character counts across renders in the correct order, and transitions can defer renders in ways that break that sequencing.

Streamdown component memoization

The top-level Streamdown component is itself wrapped in React.memo with a custom comparison. It only re-renders when these props change:
  • children
  • shikiTheme
  • isAnimating
  • animated
  • mode
  • plugins
  • className
  • linkSafety
  • lineNumbers
  • normalizeHtmlIndentation
  • literalTagContent
  • translations (compared via JSON.stringify)
  • prefix
  • dir
All other prop changes (such as mermaid, controls, or remend) do not trigger a Streamdown re-render.

useMemo for derived values

Heavy derived values are memoized with useMemo to avoid recomputation on every render:
ValueRecomputed when
processedChildrenchildren, mode, parseIncompleteMarkdown, remendOptions, allowedTags, literalTagContent change
blocksprocessedChildren or parseMarkdownIntoBlocksFn changes
mergedComponentscomponents changes
mergedRemarkPluginsremarkPlugins, plugins.math, plugins.cjk change
mergedRehypePluginsrehypePlugins, plugins.math, animatePlugin, isAnimating, allowedTags, literalTagContent change
animatePluginDerived from animated options — stable across re-renders with identical options
blockDirectionsblocksToRender, dir change
blockKeysblocksToRender.length, generatedId change

Practical performance guidelines

These guidelines apply when using Streamdown in a streaming chat interface where many messages may be rendered simultaneously.
Stabilize props that are objects or arrays. Pass stable references for components, rehypePlugins, remarkPlugins, and plugins — define them outside the render function or memoize them. Unstable references cause Block comparisons to fail and trigger unnecessary re-renders.
// Define outside the component or use useMemo
const plugins = { code };
const components = { a: CustomLink };

export default function Chat() {
  return (
    <Streamdown plugins={plugins} components={components}>
      {markdown}
    </Streamdown>
  );
}
Use mode="static" for completed messages. Once a message is fully generated, switching to static mode removes the block-parsing and transition overhead. Keep isAnimating accurate. Setting isAnimating={true} keeps the animation rehype plugin active in every render. Set it to false once streaming ends so completed messages render as plain text.

Build docs developers (and LLMs) love