React Markdown Kit vs Streamdown for AI chat
Streamdown is Vercel's Markdown component for AI output. Its readme presents it as a
replacement for react-markdown designed for AI-powered streaming, and it powers the AI
Elements message component. @react-markdown-kit/renderer is a general React
Markdown renderer whose streaming behaviour is pinned by tests. The two answer the same
question with different amounts of product in the box.
Same section order as every comparison page here: install size, GFM, security defaults, streaming, server rendering, plugins, migration. Facts about Streamdown come from its readme and its published package at version 2.6.0, vercel/streamdown. Facts about the kit link to the test or the generated table that produced them.
Install size
Bytes a browser downloads for one import, measured from published tarballs by
scripts/compare-bundles.mjs and
committed to
docs/data/bundle-sizes.json on
2026-09-20. React and React DOM are external in every row, CSS is counted separately
and never added into the JavaScript number, and 1 KB is 1024 bytes.
| Import | Version | Minified | Gzipped | License | Peer react |
|---|---|---|---|---|---|
| @react-markdown-kit/renderer, CommonMark | 0.1.0 | 118.5 KB | 36.8 KB | MIT | >=18 |
| @react-markdown-kit/renderer + GFM preset | 0.1.0 | 158.1 KB | 48.5 KB | MIT | >=18 |
| streamdown | 2.6.0 | 506.0 KB | 152.2 KB | Apache-2.0 | ^18.0.0 || ^19.0.0 |
The kit with GFM is 103.7 KB smaller gzipped
than Streamdown for the entry that was measured,
import { Streamdown } from 'streamdown'. Read that as a difference in scope, not as a
defect: Streamdown's published dependencies include marked, remend, remark-gfm,
rehype-raw, rehype-sanitize, rehype-harden, clsx and tailwind-merge, and the
component ships styled output. The kit renders unstyled elements and leaves the rest to
you.
Streamdown is also the only package compared here that is not MIT: the measured tarball declares Apache-2.0.
Two costs sit outside the JavaScript number on the Streamdown side, both from its
installation instructions: a Tailwind @source line pointing at
node_modules/streamdown/dist/*.js, and a set of shadcn/ui CSS custom properties without
which, the readme says, components may render with missing backgrounds or borders. The
kit emits no class names unless you ask for them
(styling,
docs/STYLING.md).
GFM
Both render GitHub Flavored Markdown. Streamdown lists tables, task lists and
strikethrough as built-in features and depends on remark-gfm. The kit takes one line.
import Markdown, { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'
const preset = defineMarkdownPreset({ extensions: [gfm()] })
<Markdown preset={preset}>{content}</Markdown>
The kit's GFM is asserted by
tests/gfm.test.ts: 68 cases derived from
the GFM spec, each run through the native gfm() extension, through
remarkPlugins={[remarkGfm]}, and through plain CommonMark, where the first two must
agree after normalizing attribute order, entity spelling and whitespace between block
tags (tests/helpers/html.ts) and the
third must not produce the GFM markup, unless the case is one of the 12 that assert
something is not GFM, which are named in the test so the exemption cannot spread. CommonMark itself is
covered example by example in
tests/commonmark.test.ts. No
conformance suite is run here against Streamdown.
Streamdown adds things the renderer does not have in the box: KaTeX math, Shiki syntax
highlighting, and Mermaid, each as an optional @streamdown/* plugin package in 2.6.0.
The kit has a Mermaid plugin of its own that draws flowcharts only and loads no
Mermaid.js (Mermaid in React Markdown). It ships no math
and no syntax highlighting; a rehype plugin covers both.
Security defaults
Streamdown's readme lists "Security-first" and names rehype-harden, and its published
dependencies include rehype-raw, rehype-sanitize and rehype-harden. Raw HTML
therefore goes through a sanitizer rather than being inert. This page does not test that
pipeline, so it reports the dependency list and the readme, nothing more.
The kit's default is that raw HTML is never markup until you opt in.
| Default | React Markdown Kit |
|---|---|
| Raw HTML in the source | Visible escaped text. skipHtml: true removes it. Neither executes it |
| Opting in | rehype-raw then rehype-sanitize, in that order, passed by you |
| URLs | http, https, irc, ircs, mailto, xmpp allowed, everything else emptied |
| A precompiled document | Same policy, so it is not a bypass |
Each row is a test in the renderer security tests, and the model is written up in the security model.
For model output specifically, the difference that matters is what a hallucinated
<script> or a javascript: link does. On the kit it is text and an empty href, with
no plugin installed and no configuration to get wrong.
Streaming
This is Streamdown's reason to exist, and it is the section where the kit has the evidence rather than the feature list.
Streamdown's readme describes streaming-optimized rendering with unterminated block
parsing built on remend, and memoized rendering for efficient updates. It also exposes
animated and isAnimating props for the AI SDK's streaming status.
React Markdown Kit passes, with no option to set.
packages/renderer/tests/streaming.test.tsx
is 59 tests. Seven documents are fed token by token, where words stay whole and every
whitespace and punctuation character is its own token, so a fence arrives as three
separate backticks. Every prefix is asserted on four properties:
- no prefix throws,
- the HTML of the closed prefix is byte-identical at every later prefix,
- nothing from the closed prefix is duplicated,
- the last prefix renders exactly like the whole document rendered once.
The cases are an unclosed code fence, half-written emphasis and strong, a table mid-row,
a list mid-item, a heading with no trailing newline, a link with an unclosed bracket, and
a fenced block that closes late. The same run is repeated through compileMarkdown, and
a mounted React root is grown token by token to check that a precompiled document
re-renders as it grows and ends byte-identical to a root that only ever saw the finished
document. One test asserts the heading element is reused across growth rather than
recreated.
Half-written constructs degrade to the characters typed so far: **str is the text
**str, [text]( is the text [text](, and a table is a paragraph until its delimiter
row is complete. An open fence never leaks its contents as markup, so a model writing
Markdown inside a fence does not flash headings. Two prefixes legitimately rewrite output
that already rendered, and both are pinned by tests rather than hidden: a paragraph
becomes a heading when a setext underline arrives, and a GFM autolink points at the
truncated host while the URL is still being typed.
Here is the summary you asked for.
| Metric | Value |
|---|---|
| tokens | 812 |
const partial = {
answer: 42,
The fence is still open. Its contents render as code, the finished table above it is untouched, and deleting characters from the end never throws.
The design difference is what an incomplete construct looks like. Streamdown completes
it through remend, which closes an open **, ~~ or
` and swaps an unfinished link destination for streamdown:incomplete-link
(their features list), so half-typed syntax
renders as finished formatting that changes when the real delimiter arrives. The kit
shows the literal characters and guarantees the closed prefix above does not move. If
you want completion, run remend over the string before you hand it to the kit; the kit
does not do it for you.
Server rendering
Streamdown's published dist/index.js begins with "use client", so the component runs
on the client. Its documented usage is a React client component driven by the AI SDK's
useChat.
The kit renders on the server with no DOM. Its conformance suites call
renderToStaticMarkup, and
scripts/pack-check.mjs installs the
packed tarball into a consumer with no Lexical and renders there
(server rendering). The same component works in
a React server component, during SSR and in a static build, which matters when the chat
transcript is also a page you want indexed.
The second server-side difference is compileMarkdown. A MarkdownDocument compiled
once renders many times without parsing again: on the reference machine in
benchmarks/README.md a 10 KB document
takes 16.80 ms from a string and 5.99 ms precompiled, 2.8x faster, because parsing is
about two thirds of the work. A finished chat message is exactly the case for it.
The counterweight, measured against react-markdown@10.1.0 and not against Streamdown:
1 KB is 1.21x slower (2.69 ms against 2.23 ms), 10 KB is at parity (16.22 ms against
16.25 ms), 100 KB is 0.89x (206.96 ms against 233.36 ms). Short messages are the 1 KB
case, and that row is an open regression tracked in
benchmarks/README.md. Reproduce with
pnpm bench. No benchmark against Streamdown is published here.
Plugins
Streamdown takes a plugins object of @streamdown/* packages: code, mermaid,
math, cjk in the readme example. Each is installed separately and each adds its own
Tailwind @source line.
The kit takes remarkPlugins, rehypePlugins and remarkRehypeOptions, compared prop
for prop against react-markdown at 4 of 4, 3 of 3 and 3 of 3 identical
(docs/COMPATIBILITY.md), so anything
in the unified ecosystem works. It also has extensions, which register micromark syntax,
mdast handlers, hast handlers and editor behaviour in one declaration, so the renderer,
the editor and a compile-only server read the same thing
(extensions).
That is the structural difference. Streamdown gives you a finished chat surface. The kit gives you a document contract that the renderer, the editor (editor), the Mermaid plugin and the template plugin all share, and you assemble the surface.
Migration
There is no codemod for this direction. rmk-migrate and rmk-compare handle
react-markdown only
(compare.mjs).
-import { Streamdown } from 'streamdown'
-import 'streamdown/styles.css'
+import Markdown, { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'
+
+const preset = defineMarkdownPreset({ extensions: [gfm()] })
-<Streamdown>{part.text}</Streamdown>
+<Markdown preset={preset}>{part.text}</Markdown>
What you give up, and should plan for.
- Styling. Streamdown ships styled output. The kit emits no class names by default,
so you bring a stylesheet or the opt-in
classNameshooks (styling). - Syntax highlighting and math. Shiki and KaTeX come with Streamdown's plugin packages. Here they are a rehype plugin you choose.
- Mermaid. The kit's plugin renders flowcharts only and loads no Mermaid.js (Mermaid in React Markdown).
animatedandisAnimating. No equivalent. The kit re-renders whatever string you pass and guarantees the closed prefix does not move.
What you gain: a smaller bundle, raw HTML inert with no pipeline to configure, server rendering, a precompiled document path, and the streaming behaviour written down as 59 tests you can run.
FAQ
- Which is smaller for an AI chat UI, Streamdown or React Markdown Kit?
- React Markdown Kit. One import of @react-markdown-kit/renderer 0.1.0 with the GFM preset is smaller gzipped than one import of streamdown 2.6.0, measured by scripts/compare-bundles.mjs and committed to docs/data/bundle-sizes.json. Streamdown carries syntax highlighting, styling and a hardened rehype pipeline in that number.
- Does React Markdown Kit handle partial Markdown from a model?
- Yes, and it is tested. packages/renderer/tests/streaming.test.tsx is 59 tests that feed seven documents token by token and assert that no prefix throws, the closed prefix stays byte-identical, nothing is duplicated, and the last prefix matches a one-shot render.
- Do I need Tailwind to render Markdown from an AI model?
- Not with React Markdown Kit. It emits no class names by default and ships no stylesheet you must load. Streamdown documents a Tailwind @source directive and a set of shadcn/ui CSS custom properties as part of its installation.
Next
Renderer playground · @react-markdown-kit/renderer on npm · Source on GitHub
react-markdown alternative · vs react-markdown · vs markdown-to-jsx · Renderer overview · Security model