Skip to main content

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.

One import, minified and gzipped
ImportVersionMinifiedGzippedLicensePeer react
@react-markdown-kit/renderer, CommonMark0.1.0118.5 KB36.8 KBMIT>=18
@react-markdown-kit/renderer + GFM preset0.1.0158.1 KB48.5 KBMIT>=18
streamdown2.6.0506.0 KB152.2 KBApache-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.

DefaultReact Markdown Kit
Raw HTML in the sourceVisible escaped text. skipHtml: true removes it. Neither executes it
Opting inrehype-raw then rehype-sanitize, in that order, passed by you
URLshttp, https, irc, ircs, mailto, xmpp allowed, everything else emptied
A precompiled documentSame 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:

  1. no prefix throws,
  2. the HTML of the closed prefix is byte-identical at every later prefix,
  3. nothing from the closed prefix is duplicated,
  4. 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.

A prefix a model would produce mid-answer, rendered on this page
Markdown (edit me)
Rendered

Here is the summary you asked for.

MetricValue
tokens812
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 classNames hooks (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).
  • animated and isAnimating. 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