On this page
Compiling
import Markdown, { compileMarkdown } from '@react-markdown-kit/renderer'
const doc = compileMarkdown(source, { preset: appMarkdown })
<Markdown document={doc} />compileMarkdown turns a Markdown string into a MarkdownDocument. The renderer accepts either one.
You don't need this to render Markdown. It's useful when the same document gets rendered more than once, or when parsing happens somewhere other than the component.
Why it exists#
Parsing is about two thirds of the work of rendering Markdown, so the main reason to compile is to only do it once.
It also gives the renderer, the editor and the variables plugin a common format to pass around. The renderer parses, the editor edits and the variables plugin resolves placeholders, all against the same tree. Without a shared document, each package would need its own parser, and they'd end up disagreeing.
The document shape#
interface MarkdownDocument {
readonly contractVersion: 1
readonly profile: string
readonly source?: string
readonly tree: MarkdownRoot
readonly diagnostics: readonly MarkdownDiagnostic[]
}tree is the source of truth. It's mdast (the standard Markdown syntax tree) with source positions kept.
profile records the dialect that produced the tree, such as commonmark or gfm. You'll want it in your cache key.
source is the original string, kept around for preservation and debugging. You can turn it off with retainSource: false.
diagnostics holds nonfatal problems found while parsing. It's an array, and it's usually empty.
The document is plain JSON (there are no React elements, class instances or closures in it), so you can cache it, store it or send it over the wire.
Here's a real one, compiled when this page was built:
{
"contractVersion": 1,
"profile": "commonmark",
"tree": {
"type": "root",
"children": [
{
"type": "heading",
"depth": 2,
"children": [
{
"type": "text",
"value": "Hi",
"position": {
"start": {
"line": 1,
"column": 4,
"offset": 3
},
"end": {
"line": 1,
"column": 6,
"offset": 5
}
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 6,
"offset": 5
}
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "A ",
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 7
},
"end": {
"line": 3,
"column": 3,
"offset": 9
}
}
},
{
"type": "link",
"title": null,
"url": "https://example.com",
"children": [
{
"type": "text",
"value": "link",
"position": {
"start": {
"line": 3,
"column": 4,
"offset": 10
},
"end": {
"line": 3,
"column": 8,
"offset": 14
}
}
}
],
"position": {
"start": {
"line": 3,
"column": 3,
"offset": 9
},
"end": {
"line": 3,
"column": 30,
"offset": 36
}
}
},
{
"type": "text",
"value": ".",
"position": {
"start": {
"line": 3,
"column": 30,
"offset": 36
},
"end": {
"line": 3,
"column": 31,
"offset": 37
}
}
}
],
"position": {
"start": {
"line": 3,
"column": 1,
"offset": 7
},
"end": {
"line": 3,
"column": 31,
"offset": 37
}
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 4,
"column": 1,
"offset": 38
}
}
},
"diagnostics": [],
"source": "## Hi\n\nA [link](https://example.com).\n"
}
Every node keeps its position, which is how the editor can write unchanged blocks back byte-for-byte.
isMarkdownDocument(value) is the runtime guard, and DOCUMENT_CONTRACT_VERSION is the version it checks against.
Two inputs, no precedence rule#
type MarkdownProps = MarkdownBaseProps &
({ children: string; document?: never } | { children?: never; document: MarkdownDocument })children and document are mutually exclusive in the types. Passing both is a type error, and in plain JavaScript it throws a MarkdownConfigurationError at runtime.
Since both can never be present, there's no "which one wins" rule to remember.
Performance#
Measured on a 10 kB document, server-rendered, median of the printed iteration count.
| Path | Median |
|---|---|
| From a source string | 16.80 ms |
From a precompiled MarkdownDocument | 5.99 ms |
About 2.8x faster to re-render. Reference machine is an Apple M4 Max on Node 24.17, and the method is in benchmarks/README.md.
All of the saving comes from skipping the parse. Everything after parsing still runs on every render.
What still runs#
A precompiled document doesn't get to skip the security policy.
Every syntax transform and every remark and rehype plugin still runs, and so do the URL policy, the element filter and the raw-HTML rule.
Passing a document only skips parsing.
The document you pass in is also cloned before rendering, so rendering won't mutate the object you cached.
Errors and diagnostics#
Problems with the content show up as diagnostics on the document, so you don't need to wrap ordinary Markdown in a try.
Configuration mistakes (like an invalid preset or a non-string source) throw MarkdownConfigurationError.
import { MarkdownConfigurationError } from '@react-markdown-kit/renderer'Compilation is deterministic for the same source and configuration, and it doesn't touch the network.
Back to Markdown#
import { compileMarkdown, documentToMarkdown } from '@react-markdown-kit/renderer'
const doc = compileMarkdown(source, { preset: appMarkdown })
const markdown = await documentToMarkdown(doc, { preset: appMarkdown })documentToMarkdown returns a promise because the serializer is loaded on demand, which keeps it out of a render-only bundle.
The serializer normalizes the output to a canonical form. A setext heading becomes an ATX heading, and a ~~~ fence becomes a backtick fence. The meaning stays the same, and serializing twice gives the same bytes as serializing once.
Pass the same preset to both calls, since an extension that adds syntax also brings the rule for writing it back.
If you need the exact original bytes instead of a canonical form, the editor handles that. See Round-trip preservation.
Where compiling pays#
A server that renders the same article over and over. Compile at publish time, cache the document and render it per request.
A list of lots of small documents. Compile each one once and keep the documents in the list state.
Variables. The variables plugin already resolves into a document and hands it straight to the renderer.
## Hi A [link](https://example.com).
Hi
A link.
This example renders from a string. Passing document={compileMarkdown(source)} gives you the same tree, runs the same policy pass and produces the same DOM.
Related#
You can set the dialect a document compiles with in a preset.
To read the rendered node metadata, use a component override.
Read next#
The whole syntax on one page you can edit: Renderer demo.
Last updated on