TL;DR
For most content pipelines in 2026: remark (with unified) is the most powerful choice — it parses Markdown into an AST, enabling sophisticated transformations, plugins, and MDX support. marked is the fastest option for simple HTML conversion with minimal dependencies. markdown-it hits the sweet spot between speed and extensibility, with a large plugin ecosystem. The right choice depends on whether you need to transform Markdown or just render it.
Key Takeaways
- marked: ~12M weekly downloads — fastest, CommonMark-compliant, battle-tested
- markdown-it: ~21M weekly downloads — most downloaded, extensible, used by VS Code
- remark: ~8M weekly downloads — AST-based, plugin ecosystem, powers MDX/Astro/Next.js
- markdown-it's massive download count includes VS Code's dependency (transitive downloads)
- marked wins for raw speed; remark wins for transformations and MDX; markdown-it wins for plugin ecosystem
- For Next.js/Astro/MDX pipelines: remark is already there — use it
Download Trends
| Package | Weekly Downloads | Used By | Approach |
|---|---|---|---|
markdown-it | ~21M | VS Code, Docusaurus | Renderer-based |
marked | ~12M | Many tools | Renderer-based |
remark | ~8M | Next.js, Astro, MDX | AST (unified) |
markdown-it's 21M downloads are heavily inflated by VS Code's transitive dependency.
The Three Approaches
Understanding the architectural difference:
Renderer-based (marked, markdown-it):
Markdown string → parse tokens → render HTML directly
Fast, but limited transformation options
AST-based (remark/unified):
Markdown string → parse → MDAST (abstract syntax tree) → plugins → HTML/MDX/etc.
Slower, but unlimited transformation capability
marked
marked was created in 2011 and optimized for speed. It's the simplest path from Markdown string to HTML:
import { marked, Renderer, Tokenizer } from "marked"
// Basic conversion (synchronous):
const html = marked("# Hello World\n\nThis is **bold** text.")
// → "<h1>Hello World</h1><p>This is <strong>bold</strong> text.</p>"
// With options:
marked.use({
gfm: true, // GitHub Flavored Markdown (tables, strikethrough)
breaks: false, // Convert \n to <br>
})
// Custom renderer — override how elements are rendered:
const renderer = new Renderer()
renderer.link = (href, title, text) => {
const external = href.startsWith("http")
return external
? `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`
: `<a href="${href}">${text}</a>`
}
renderer.code = (code, language) => {
// Integrate with a syntax highlighting library:
const highlighted = hljs.highlight(code, { language: language || "plaintext" }).value
return `<pre><code class="language-${language}">${highlighted}</code></pre>`
}
marked.use({ renderer })
// Async extension example:
const tokenizer = new Tokenizer()
tokenizer.paragraph = (src) => {
// Override paragraph tokenizer
return { type: "paragraph", text: src }
}
marked's async API (for content pipelines):
// Async marked with walkTokens:
marked.use({
async walkTokens(token) {
if (token.type === "code" && token.lang === "sql") {
// Transform SQL code blocks — async operations allowed
token.text = await formatSQL(token.text)
}
}
})
const html = await marked.parse(content)
marked with sanitization:
// marked doesn't sanitize — combine with DOMPurify:
import DOMPurify from "dompurify"
const rawHtml = await marked.parse(userContent)
const safeHtml = DOMPurify.sanitize(rawHtml)
markdown-it
markdown-it is the most CommonMark-compliant parser with excellent plugin support:
import MarkdownIt from "markdown-it"
import markdownItAnchor from "markdown-it-anchor"
import markdownItTocDoneRight from "markdown-it-toc-done-right"
import markdownItPrism from "markdown-it-prism"
import markdownItKatex from "@vscode/markdown-it-katex"
import markdownItContainer from "markdown-it-container"
const md = new MarkdownIt({
html: true, // Allow HTML in markdown
linkify: true, // Auto-convert URLs to links
typographer: true, // Smart quotes, dashes, etc.
})
.use(markdownItAnchor, {
permalink: markdownItAnchor.permalink.headerLink(),
slugify: (s) => s.toLowerCase().replace(/\s/g, "-"),
})
.use(markdownItTocDoneRight, { containerClass: "table-of-contents" })
.use(markdownItPrism) // Prism syntax highlighting
.use(markdownItKatex) // LaTeX math support
.use(markdownItContainer, "tip", {
validate: (params: string) => params.trim().match(/^tip\s*/),
render: (tokens: any[], idx: number) => {
return tokens[idx].nesting === 1
? '<div class="tip">\n'
: '</div>\n'
},
})
const html = md.render("# Hello World\n\n$$E = mc^2$$\n\n::: tip\nPro tip here!\n:::")
markdown-it token-level customization:
// Override the default image rendering:
const defaultImageRender = md.renderer.rules.image ?? MarkdownIt.renderToken.bind(md.renderer)
md.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx]
const src = token.attrGet("src")
const alt = token.attrGet("alt") || ""
const title = token.attrGet("title") || ""
// Custom image component with lazy loading:
return `<img src="${src}" alt="${alt}" title="${title}" loading="lazy" decoding="async" />`
}
markdown-it's plugin ecosystem:
markdown-it-anchor— Auto-generate heading anchor linksmarkdown-it-toc-done-right— Table of contents generationmarkdown-it-prism/markdown-it-shiki— Syntax highlightingmarkdown-it-katex/@vscode/markdown-it-katex— LaTeX mathmarkdown-it-container— Custom container blocks (:::: tip ::::)markdown-it-attrs— Add classes and attributes to elementsmarkdown-it-footnote— Footnote supportmarkdown-it-emoji— Emoji shortcodes (:rocket: 🚀)markdown-it-task-lists— GitHub-style task lists
VS Code uses markdown-it as its core Markdown renderer.
remark (unified)
remark is part of the unified ecosystem — it parses Markdown into an AST (MDAST) and processes it through plugins:
import { unified } from "unified"
import remarkParse from "remark-parse"
import remarkGfm from "remark-gfm"
import remarkMath from "remark-math"
import remarkRehype from "remark-rehype"
import rehypeKatex from "rehype-katex"
import rehypePrettyCode from "rehype-pretty-code"
import rehypeStringify from "rehype-stringify"
import rehypeSlug from "rehype-slug"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
// Process pipeline:
const processor = unified()
.use(remarkParse) // Parse Markdown → MDAST
.use(remarkGfm) // GitHub Flavored Markdown
.use(remarkMath) // Math blocks
.use(remarkRehype, { allowDangerousHtml: true }) // MDAST → HAST (HTML AST)
.use(rehypeKatex) // Render math with KaTeX
.use(rehypePrettyCode, { // Syntax highlighting (Shiki)
theme: "github-dark",
keepBackground: false,
})
.use(rehypeSlug) // Add IDs to headings
.use(rehypeAutolinkHeadings) // Link headings to themselves
.use(rehypeStringify) // HAST → HTML string
const result = await processor.process(markdownContent)
const html = result.toString()
Inspecting and modifying the AST:
import { visit } from "unist-util-visit"
// Custom remark plugin — transform the AST directly:
function remarkAddTableCaption() {
return (tree: any) => {
visit(tree, "table", (node, index, parent) => {
// Find the paragraph before the table:
const prevSibling = parent?.children[index - 1]
if (prevSibling?.type === "paragraph") {
const text = prevSibling.children[0]?.value
if (text?.startsWith("Table:")) {
node.data = {
...node.data,
caption: text.replace("Table: ", ""),
}
}
}
})
}
}
// Custom rehype plugin — transform HTML AST:
function rehypeWrapImages() {
return (tree: any) => {
visit(tree, "element", (node) => {
if (node.tagName === "img") {
// Wrap images in a figure element:
const figure = {
type: "element",
tagName: "figure",
properties: {},
children: [node, {
type: "element",
tagName: "figcaption",
properties: {},
children: [{ type: "text", value: node.properties.alt || "" }],
}],
}
Object.assign(node, figure)
}
})
}
}
remark in Next.js (contentlayer / next-mdx-remote):
// next-mdx-remote example:
import { MDXRemote } from "next-mdx-remote/rsc"
import remarkGfm from "remark-gfm"
import rehypePrettyCode from "rehype-pretty-code"
export default async function BlogPost({ content }: { content: string }) {
return (
<MDXRemote
source={content}
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [[rehypePrettyCode, { theme: "github-dark" }]],
},
}}
/>
)
}
remark is used by: Next.js, Astro, Gatsby, MDX, Docusaurus, and most modern static site generators.
Feature Comparison
| Feature | marked | markdown-it | remark |
|---|---|---|---|
| CommonMark compliance | ✅ | ✅ Best | ✅ |
| GFM (tables, etc.) | ✅ | ✅ Plugin | ✅ Plugin |
| Speed (benchmark) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Plugin ecosystem | ⚠️ Small | ✅ Large | ✅ Largest |
| AST access | ❌ | ⚠️ Token-level | ✅ Full AST |
| MDX support | ❌ | ❌ | ✅ |
| Syntax highlighting | Via renderer | Via plugin | Via rehype |
| TypeScript | ✅ | ✅ | ✅ |
| Async support | ✅ walkTokens | ❌ | ✅ |
| HTML sanitization | ❌ External | ❌ External | ❌ External |
| SSR compatible | ✅ | ✅ | ✅ |
When to Use Each
Choose remark if:
- Building a blog, documentation site, or content pipeline in Next.js/Astro
- You need MDX (React components in Markdown)
- AST manipulation is required (custom transformations)
- You want the most comprehensive plugin ecosystem
Choose markdown-it if:
- You need a battle-tested, CommonMark-compliant renderer
- The VS Code plugin ecosystem or custom container syntax is needed
- You're building a documentation tool or CMS editor preview
- Speed matters more than AST flexibility
Choose marked if:
- You need the fastest Markdown-to-HTML conversion
- Minimal dependencies are priority
- Simple rendering without complex transformations
- Real-time preview (editor, comment renderer)
Sanitization Warning
None of these libraries sanitize HTML output by default. For user-generated content:
import DOMPurify from "dompurify"
import { JSDOM } from "jsdom" // For Node.js
// Server-side sanitization:
const window = new JSDOM("").window
const purify = DOMPurify(window)
const rawHtml = await marked.parse(userInput)
const safeHtml = purify.sanitize(rawHtml, {
ALLOWED_TAGS: ["p", "strong", "em", "a", "code", "pre", "ul", "ol", "li", "h1", "h2", "h3"],
ALLOWED_ATTR: ["href", "class"],
})
Syntax Highlighting Integration
Syntax highlighting is the most-requested Markdown feature in documentation and blog sites, and each library integrates with highlighters differently. With marked, the standard approach is a custom renderer that intercepts code blocks and passes them to highlight.js or a similar synchronous highlighter. The custom code renderer receives the raw source and detected language, and returns the highlighted HTML string. This works well but ties you to synchronous highlighting libraries — async options like Shiki (which loads WASM and theme files) require marked's walkTokens async mode, which is less commonly documented.
markdown-it integrates with highlighters via both the highlight option in the constructor and dedicated plugins. The markdown-it-shiki plugin provides first-class Shiki integration with async initialization and caching. The markdown-it-prism plugin wraps Prism.js with minimal configuration. Because markdown-it's plugin system gives plugins direct access to the token rendering pipeline, integration is cleaner than the renderer override approach in marked — plugins can also annotate code blocks with metadata (line numbers, diff highlights, filename headers) that enriches the generated HTML.
Remark's syntax highlighting integrates at the rehype layer via rehype-pretty-code (which wraps Shiki) or rehype-highlight (which wraps highlight.js). The AST-based approach means the highlighter sees a structured representation of the code block — language, content, and optional meta attributes like filename="main.ts" or {2-4} line highlight ranges — rather than a raw string. rehype-pretty-code is the de facto standard for Next.js and Astro documentation sites in 2026 because it supports Shiki's dual-theme mode (light/dark), line-level and word-level highlighting, and diff syntax. If syntax highlighting quality is a priority for your content pipeline, the remark + rehype-pretty-code combination produces noticeably better output than marked or markdown-it plugins.
Streaming and Edge Runtime Considerations
Documentation systems increasingly render Markdown at the edge — in Cloudflare Workers, Vercel Edge Functions, or Next.js middleware — where both bundle size and WASM compatibility matter. Each library has different edge compatibility characteristics.
Marked is the most edge-compatible: it's pure JavaScript, ~50KB before minification, and has no native dependencies. It runs in any V8-based environment including Cloudflare Workers without configuration. The lack of WASM or dynamic require() calls means it passes Cloudflare's strict import analysis without issues.
markdown-it is similarly pure JavaScript and edge-compatible. Its larger plugin ecosystem does introduce risk — some community plugins use require() or Node.js built-ins that aren't available at the edge — but the core library itself works cleanly. Bundle size is slightly larger than marked but still reasonable for edge deployment.
Remark's unified ecosystem has the largest bundle footprint because the processor chain includes multiple plugins, each adding to the final size. More critically, rehype-pretty-code and its Shiki dependency load WASM files that require specific handling in Cloudflare Workers (WASM modules must be imported statically, not dynamically). This makes the remark + Shiki combination non-trivial to deploy at the edge without build-time processing. For edge rendering, marked or markdown-it are more practical. For build-time rendering (Next.js generateStaticParams, Astro's content collections), remark with rehype-pretty-code is the better choice with no edge constraints.
User-Generated Content and XSS
All three libraries output raw HTML and none of them sanitize by default. This is the correct default for static site generators where content comes from trusted authors, but it is a critical security risk for user-generated content. An attacker who can inject Markdown into your application can execute arbitrary JavaScript in a reader's browser through <script> tags, javascript: href values, or onerror image attributes embedded in the Markdown source.
The correct mitigation is always a dedicated HTML sanitizer after rendering, not configuration flags on the Markdown parser. DOMPurify is the community standard: it uses the browser's own HTML parser to build a DOM tree, then strips any element or attribute not on the allowlist. For server-side Node.js rendering, DOMPurify works with jsdom's window object. Both isomorphic-dompurify and the DOMPurify + jsdom combination are well-tested patterns. The key is to sanitize the rendered HTML string — not the Markdown input — because some XSS vectors survive Markdown parsing and appear only in the HTML output. Never assume that restricting Markdown syntax (no raw HTML allowed) is a sufficient substitute for output sanitization.
Methodology
Download data from npm registry (weekly average, February 2026). Speed benchmarks are approximate based on community benchmarks at typical blog post length (~2000 words). Feature comparison based on marked 12.x, markdown-it 14.x, and remark 15.x / unified 11.x.
Compare Markdown parser packages on PkgPulse →
See also: AVA vs Jest and Mermaid vs D3.js vs Chart.js 2026, acorn vs @babel/parser vs espree.