Comparison
Honest performance benchmarks and feature comparison against react-markdown.
@deltakit/markdown is faster and smaller than react-markdown, but it also does less. This page presents honest numbers with full context so you can make an informed decision.
Bundle Size
This is the most objective comparison — no caveats needed.
| Metric | @deltakit/markdown | react-markdown |
|---|---|---|
| Minified | 12.0kb | 114.8kb |
| Minified + gzipped | 3.8kb | 35.3kb |
| Runtime dependencies | 0 | 11 |
react-markdown is 9.3x larger because it bundles the full unified/remark/rehype pipeline. @deltakit/markdown has zero runtime dependencies — the parser is built from scratch.
Note on streaming stability:
@deltakit/markdownbuffers partial block-level markers and early table lines during streaming to prevent choppy rendering. Empty headings, orphan list bullets, and standalone table headers never flash in the DOM.
Render Performance
Both libraries render the same markdown content via renderToStaticMarkup. Measured with vitest bench (tinybench).
| Benchmark | @deltakit/markdown | react-markdown | Speedup |
|---|---|---|---|
| Short paragraph (~60 chars) | 174,000 ops/s | 25,000 ops/s | ~7.0x |
| Mixed content (~1.2kb) | 6,825 ops/s | 1,040 ops/s | ~6.6x |
| Streaming simulation (141 re-renders) | 76 ops/s | 16 ops/s | ~4.7x |
Caveats
These numbers are real, but context matters:
- @deltakit/markdown produces clean semantic HTML. Blocks render directly as semantic elements (
<h1>,<p>,<pre>, etc.) without wrapper<div>or<span>elements. Less DOM work naturally means faster rendering. This is not a same-output comparison. - react-markdown runs a full CommonMark pipeline. It parses via micromark, transforms through the remark/rehype AST pipeline, and produces spec-compliant HTML. We run a simplified regex-based parser. Being faster while doing less work is expected, not remarkable.
- The streaming simulation uses
renderToStaticMarkup. This does NOT demonstrate ourReact.memoblock memoization advantage. In a real browser with React reconciliation, settled blocks would usually be skipped unless later parsing extended the same block — but that's difficult to benchmark accurately in a synthetic test.
Parse-Only Performance
We also benchmark our parser (parseIncremental) against micromark, the parser engine that react-markdown uses internally.
| Benchmark | @deltakit/markdown | micromark | Speedup |
|---|---|---|---|
| Short paragraph | 3,720,000 ops/s | 26,500 ops/s | ~140x |
| Mixed content (~1.2kb) | 219,700 ops/s | 1,351 ops/s | ~163x |
| Streaming sim (141 parses) | 2,080 ops/s | 20.4 ops/s | ~102x |
Caveats
- Different output formats.
parseIncrementalproduces aBlock[]array (lightweight AST). micromark produces a full HTML string with spec-compliant output. These are fundamentally different amounts of work. - Different feature coverage. micromark implements the full CommonMark specification. Our parser implements a subset. Fewer features means fewer code paths means faster execution.
The parse-only numbers show our parser is fast, but the magnitude of the speedup is partly because we're solving a simpler problem.
Performance at Scale
@deltakit/markdown is optimized for long chat transcripts with many rendered messages. Key performance characteristics:
| Scenario | Threshold |
|---|---|
| Parse ~15KB document (mixed blocks) | < 50ms |
| Parse 50 messages in sequence | < 2s |
| Incremental stream parse (per 100-char step) | < 10ms |
| 200-item list parse | < 30ms |
| 50KB+ document parse | < 100ms |
DOM Efficiency
The renderer produces clean semantic HTML with no wrapper bloat:
- No wrapper
<div>around blocks — headings, paragraphs, code, lists render as direct semantic elements - No wrapper
<span>around inline tokens — text, bold, italic, code render without extra span wrappers - Single shared
<style>tag — streaming CSS is injected once via a ref-counted singleton, not per component instance - Static
Markdowncomponent — for completed messages, skips all streaming overhead (batching, debouncing, opacity transitions)
These optimizations keep the browser layout/paint/style-recalculation cost low even with hundreds of rendered messages.
Feature Comparison
This is where react-markdown clearly has the advantage. It's a mature, battle-tested library with full spec compliance.
Features both libraries support
| Feature | @deltakit/markdown | react-markdown |
|---|---|---|
| Headings (h1-h6) | Yes | Yes |
| Paragraphs | Yes | Yes |
Bold (**text**) | Yes | Yes |
Italic (*text*) | Yes | Yes |
| Inline code | Yes | Yes |
| Fenced code blocks | Yes | Yes |
| Links | Yes | Yes |
| Images | Yes | Yes |
| Unordered lists | Yes | Yes |
| Ordered lists | Yes | Yes |
| Blockquotes | Yes | Yes |
| Horizontal rules | Yes | Yes |
| Strikethrough | Yes | Via remark-gfm |
| Tables | Partial | Via remark-gfm |
| Autolinks | Yes | Yes |
Features only react-markdown supports
| Feature | Status in @deltakit/markdown |
|---|---|
| Full CommonMark compliance | Not supported |
| Nested blockquotes | Not supported |
Reference links ([text][id]) | Not supported |
HTML entities (&, —) | Not supported |
Escape sequences (\*not italic\*) | Not supported |
| Plugin ecosystem (remark/rehype) | Not supported |
HTML passthrough (rehypeRaw) | Not supported |
Features only @deltakit/markdown supports
| Feature | Description |
|---|---|
Block-level React.memo | Settled blocks normally avoid re-rendering while the active block updates |
| Incomplete syntax buffering | Raw **, `, [, and incomplete table starts stay hidden during streaming |
| Code block skeleton | Pending code blocks render as empty <pre> shells, not paragraphs |
Render batching (batchMs) | Configurable DOM update frequency (8ms, 16ms, 32ms) |
Static Markdown component | Lightweight renderer for completed messages — no batching, debouncing, or streaming styles |
| Headless hook | useStreamingMarkdown for custom rendering |
| Framework-agnostic parser | parseIncremental from @deltakit/markdown/core |
| Singleton style injection | One shared <style> tag regardless of instance count |
| Zero runtime dependencies | No remark, rehype, unified, micromark, vfile |
When to Use Which
Use @deltakit/markdown when:
- You're rendering AI-streamed markdown in real time
- Flicker-free rendering matters to your users
- Bundle size is a concern
- You don't need the full CommonMark spec or plugin ecosystem
Use react-markdown when:
- You're rendering static (non-streaming) markdown
- You need full CommonMark compliance
- You need remark/rehype plugins (math, syntax highlighting, sanitization)
- You need HTML passthrough support
Both are good tools. They solve different problems.
Running the Benchmarks
The benchmarks are included in the package source. To run them yourself:
cd packages/markdown
pnpm run benchTo save results as JSON for regression tracking:
pnpm run bench:saveResults are written to benchmarks/results/latest.json.