Block-level memoization
In streaming mode, Streamdown splits the markdown into top-level blocks (paragraphs, headings, code fences, etc.) and renders each as a separateBlock 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 stringindex— the block’s position in the arrayisIncomplete— whether the block is still being streameddir— resolved text directionshouldNormalizeHtmlIndentation— HTML normalization settingcomponents— checked with key-level shallow equality (different reference is OK as long as the component values match)rehypePlugins— reference equalityremarkPlugins— reference equality
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.memothen gates
React Transitions for streaming updates
In streaming mode (without theanimated prop), block state updates are wrapped with startTransition:
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-levelStreamdown component is itself wrapped in React.memo with a custom comparison. It only re-renders when these props change:
childrenshikiThemeisAnimatinganimatedmodepluginsclassNamelinkSafetylineNumbersnormalizeHtmlIndentationliteralTagContenttranslations(compared viaJSON.stringify)prefixdir
mermaid, controls, or remend) do not trigger a Streamdown re-render.
useMemo for derived values
Heavy derived values are memoized withuseMemo to avoid recomputation on every render:
| Value | Recomputed when |
|---|---|
processedChildren | children, mode, parseIncompleteMarkdown, remendOptions, allowedTags, literalTagContent change |
blocks | processedChildren or parseMarkdownIntoBlocksFn changes |
mergedComponents | components changes |
mergedRemarkPlugins | remarkPlugins, plugins.math, plugins.cjk change |
mergedRehypePlugins | rehypePlugins, plugins.math, animatePlugin, isAnimating, allowedTags, literalTagContent change |
animatePlugin | Derived from animated options — stable across re-renders with identical options |
blockDirections | blocksToRender, dir change |
blockKeys | blocksToRender.length, generatedId change |
Practical performance guidelines
These guidelines apply when using Streamdown in a streaming chat interface where many messages may be rendered simultaneously.
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.
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.