Beta

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.

Explore the docs

Markdown rendering benchmarks

From everyday AI replies to full specifications, compare rendering speed, streaming work, memory use, and download size.

How we measure ↓

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.

Speed
PreviewReady

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.

376 / 376 charactersEdit the source or replay the response.
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.

PreviewReady

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.

376 / 376 charactersEdit the source or replay the response.
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.

PreviewReady

From a thought to a function

A tiny generator, with a little room to breathe.

typescript
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.

461 / 461 charactersEdit the source or replay the response.
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.

PreviewReady

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
303 / 303 charactersEdit the source or replay the response.
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.

Editable
PreviewReady

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
tsx
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

  1. Even the little ones at the bottom of the page. ↩

987 / 987 charactersEdit the source or replay the response.
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>