Stream words and markdown
@pierre/words is a super-fast, lightweight Markdown renderer hand-written in WebAssembly Text. Built for streaming conversations. Renders directly to HTML, supports GitHub Flavored Markdown, and pairs with @pierre/highlights for syntax highlighting. Includes incremental updates, HTML render hooks, and optional token fades. The base React renderer is 69% smaller than Streamdown in our gzip benchmark.
Markdown rendering benchmarks
From everyday AI replies to full specifications, compare rendering speed, streaming work, memory use, and download size.
Markdown to HTML and React
How much Markdown becomes HTML per second. Higher is better.
AI code review · 5.4 KiB · A code review response with suggested diffs and TypeScript examples.
- Words
- 382.6MiB/s
- md4x (NAPI)
- 198.9MiB/s
- md4x (Wasm)
- 158.6MiB/s
- markdown-it
- 21.8MiB/s
Streaming in the browser
A long response with paragraphs and TypeScript code, tested on its own and repeated 100 times to show how each renderer scales.
Streaming updates
Time for incoming text to reach the DOM, before paint. 95% of updates finish within this time. Lower is better.
- Words
- 2ms
- Streamdown
- 26.9ms
Memory after streaming
JavaScript memory kept after the full document has rendered. Lower is better.
- Words
- 5.7MB
- Streamdown
- 142.9MB
Browser download
JavaScript transferred to load the renderer and selected languages, compressed with gzip. Lower is better.
- Words
- 247KB
- Streamdown
- 1,057.8KB
How we measure
HTML and React benchmarks
Five documents cover short Markdown, AI responses, and technical specifications. HTML throughput measures input MiB per second. React replays deliver chunks of up to 256 UTF-16 code units in JSDOM, without highlighting or animations. This measures parsing and DOM work; it does not include browser layout or paint. Memory is median peak process growth through import and rendering, measured in five fresh processes per engine and document.
November 10, 2026 · Node v24.11.0 · Apple M4 Max · 128 GiB RAM · commit 8b0ad8f3. md4x 0.0.29, markdown-it 15.0.0, React 19.2.7, Streamdown 2.7.1, JSDOM 26.1.0.
Browser streaming and memory
The 8.5 KB document has 100 paragraphs and a 40-line TypeScript block; the 853 KB document repeats it 100 times. Both libraries use their native streaming paths, with 267 updates spaced 40 ms after each commit. Results are the median of 3 fresh-browser runs, after warming startup and grammar loading, in a 1280 × 960 viewport. Auto-scrolling, code controls, and line numbers are off.
Memory is settled V8 heap growth after forced garbage collection; it excludes native DOM, Wasm, and graphics memory. Repeated code blocks are identical; unique code and other languages may differ. Small documents stay near the display frame interval for both renderers. Words fades delivered chunks, including code; Streamdown fades prose words. Single 8.53 MB diagnostic runs stutter in both libraries and are excluded from the charts.
November 10, 2026 · Apple M4 Max · 128 GiB RAM · Chromium 134.0.6998.35 · Bun 1.4.0 · React 19.2.7 · Streamdown 2.7.1 · commit 8b0ad8f3.
Browser download
Minified JavaScript, gzipped per file, with React external. The base renderer counts initial static imports; the language total counts every chunk loaded for the same 148 languages and GitHub light/dark themes. Words includes all 152 built-in languages. Streamdown loads languages on demand, including required embedded grammars. Streamdown downloads less than this total on pages that use fewer languages. CSS is separate: 1.2 KB for Words and 0.4 KB for Streamdown.
November 11, 2026 · Bun 1.4.0 · commit cb5b6305.
Explore or reproduce the results
The benchmark source includes setup instructions, commands, and the Markdown documents used here. Raw results include frame timings and the larger capacity diagnostics.
A softer arrival
Fade in new text with animated. Existing text keeps its place, and reduced-motion preferences are respected. Adjust the duration and replay to find the right pace.
A little room for better ideas
Good tools get out of your way. Great tools make you want to keep going.
This response arrives a few characters at a time. Watch emphasis, links, and code find their shape.
- Start with an idea
- Add a little more detail
- Keep the conversation going
The best part of a good tool is what you make with it.
Use it in React
import { Markdown } from '@pierre/words/react';
import '@pierre/words/styles.css';
<Markdown
isAnimating={isStreaming}
animated={{ animation: 'fadeIn', duration: 180 }}
lineNumbers={false}
>
{text}
</Markdown>Built for the conversation
Stream a response without waiting for the final token. Emphasis, links, lists, and late references settle into place as text arrives.
A little room for better ideas
Good tools get out of your way. Great tools make you want to keep going.
This response arrives a few characters at a time. Watch emphasis, links, and code find their shape.
- Start with an idea
- Add a little more detail
- Keep the conversation going
The best part of a good tool is what you make with it.
Use it in React
import { Markdown } from '@pierre/words/react';
import '@pierre/words/styles.css';
<Markdown
isAnimating={isStreaming}
animated={{ animation: 'fadeIn', duration: 180 }}
lineNumbers={false}
>
{text}
</Markdown>Code that keeps up
Add @pierre/highlights to highlight code as it streams. Toggle line numbers and replay the example to watch it take shape.
From a thought to a function
A tiny generator, with a little room to breathe.
async function* words(message: string) {
for (const word of message.split(' ')) {
yield word + ' ';
await new Promise((resolve) => setTimeout(resolve, 60));
}
}
for await (const word of words('Hello, world.')) {
process.stdout.write(word);
}
Inline code, like yield word, gets its own treatment. Unknown languages fall back to plain text.
Use it in React
import { Markdown } from '@pierre/words/react';
import '@pierre/words/styles.css';
import { createHighlightsPlugin } from '@pierre/words/highlights';
import light from '@pierre/highlights/themes/pierre-light';
import dark from '@pierre/highlights/themes/pierre-dark';
const plugins = {
code: createHighlightsPlugin({ themes: { light, dark }, defaultColor: 'light-dark()' }),
};
<Markdown
isAnimating={isStreaming}
animated={{ animation: 'fadeIn', duration: 180 }}
plugins={plugins}
>
{text}
</Markdown>Your HTML, your way
Customize the generated HTML with string hooks. Switch between the default output and styled headings, links, and inline code without changing the Markdown.
HTML render hooks
Make links feel at home in your app. Style inline code, add an icon to headings, and keep the same Markdown.
Keep your own style
- Return HTML strings from your hooks
- Let Words handle the streaming updates
- Use your own classes and design tokens
Use it in React
import { Markdown } from '@pierre/words/react';
import '@pierre/words/styles.css';
<Markdown
isAnimating={isStreaming}
animated={{ animation: 'fadeIn', duration: 180 }}
lineNumbers={false}
components={{
a: (html) => html.replace('<a ', '<a class="your-link" '),
}}
>
{text}
</Markdown>Take it for a longer run
Try your own Markdown or choose a larger document. Large samples wait for you to replay the stream before rendering their preview.
A little room for better ideas
Good tools get out of your way. Great tools make you want to keep going.
This response arrives a few characters at a time. Watch emphasis, links, and code find their shape.
Built for the conversation
- Write naturally, with GitHub Flavored Markdown
- Customize the generated HTML
- Make something worth sharing
import { Markdown } from '@pierre/words/react';
export function Message({ text, streaming }) {
return (
<Markdown isAnimating={streaming}>
{text}
</Markdown>
);
}
| A familiar idea | A thoughtful detail |
|---|---|
| Markdown | Tables, tasks, and footnotes |
| Streaming | Formatting before the final token |
| Code | Highlighted by Pierre Highlights |
The best part of a good tool is what you make with it.
Keep exploring Pierre. Every detail counts.1
-
Even the little ones at the bottom of the page. ↩
Use it in React
import { Markdown } from '@pierre/words/react';
import '@pierre/words/styles.css';
import { createHighlightsPlugin } from '@pierre/words/highlights';
import light from '@pierre/highlights/themes/pierre-light';
import dark from '@pierre/highlights/themes/pierre-dark';
const plugins = {
code: createHighlightsPlugin({ themes: { light, dark }, defaultColor: 'light-dark()' }),
};
<Markdown
isAnimating={isStreaming}
animated={{ animation: 'fadeIn', duration: 180 }}
plugins={plugins}
>
{text}
</Markdown>