marked vs remark vs markdown-it: Markdown Parsers in JavaScript (2026)
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"],
})
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.