Best Markdown Parsing Libraries for JavaScript in 2026
·PkgPulse Team
TL;DR
marked for speed and simplicity; remark/unified for plugin-rich content pipelines. marked (~15M weekly downloads) is the fastest Markdown-to-HTML converter — parse a large doc in under 1ms, great for simple rendering. remark (~8M downloads) is the center of the unified ecosystem with 300+ plugins for linting, transforming, and generating documentation. markdown-it (~20M downloads) is the most popular by downloads with excellent plugin support and spec compliance.
Key Takeaways
- markdown-it: ~20M weekly downloads — spec-compliant, widely used, great plugin support
- marked: ~15M downloads — fastest, simplest API, GitHub-flavored Markdown
- remark: ~8M downloads — unified ecosystem, 300+ plugins, AST-based transformations
- marked + DOMPurify — always sanitize HTML output in browser contexts
- remark — powers MDX, Docusaurus, Gatsby, most static site generators
markdown-it (Most Popular)
// markdown-it — configurable, spec-compliant
import markdownIt from 'markdown-it';
const md = new markdownIt({
html: false, // Allow HTML tags in source
linkify: true, // Autoconvert URL-like text to links
typographer: true, // Enable some language-neutral typographic replacements
breaks: false, // Convert '\n' in paragraphs to <br>
highlight: (str, lang) => {
// Syntax highlighting with highlight.js or shiki
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(str, { language: lang }).value;
}
return '';
},
});
const html = md.render('# Hello World\n\nThis is **markdown**.');
// <h1>Hello World</h1>
// <p>This is <strong>markdown</strong>.</p>
// markdown-it — plugins
import markdownIt from 'markdown-it';
import markdownItAnchor from 'markdown-it-anchor';
import markdownItToc from 'markdown-it-table-of-contents';
import markdownItFootnote from 'markdown-it-footnote';
import markdownItContainer from 'markdown-it-container';
const md = new markdownIt()
.use(markdownItAnchor, {
permalink: markdownItAnchor.permalink.ariaHidden({ placement: 'before' }),
})
.use(markdownItToc, { includeLevel: [2, 3] })
.use(markdownItFootnote)
.use(markdownItContainer, 'warning', {
render: (tokens, idx) => {
return tokens[idx].nesting === 1
? '<div class="warning">\n'
: '</div>\n';
},
});
const html = md.render(`
[[toc]]
# Title
## Section 1
Text with footnote.[^1]
::: warning
This is a warning container.
:::
[^1]: The footnote text.
`);
marked (Fastest)
// marked — simple, fast Markdown to HTML
import { marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import hljs from 'highlight.js';
// Configure once, use everywhere
marked.use(
markedHighlight({
langPrefix: 'hljs language-',
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
},
})
);
// Parse Markdown to HTML
const html = marked('# Hello\n\nWorld!');
// <h1>Hello</h1><p>World!</p>
// Async version (for async rendering extensions)
const htmlAsync = await marked.parse('# Hello');
// marked — custom renderer
import { marked, Renderer } from 'marked';
const renderer = new Renderer();
// Custom link renderer — add target="_blank" to external links
renderer.link = (href, title, text) => {
const isExternal = href?.startsWith('http');
const titleAttr = title ? ` title="${title}"` : '';
const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
return `<a href="${href}"${titleAttr}${target}>${text}</a>`;
};
// Custom heading — add IDs
renderer.heading = (text, level) => {
const id = text.toLowerCase().replace(/[^\w]+/g, '-');
return `<h${level} id="${id}">${text}</h${level}>\n`;
};
marked.use({ renderer });
const html = marked('# My Title\n\n[Google](https://google.com)');
// marked — IMPORTANT: sanitize in browser!
import { marked } from 'marked';
import DOMPurify from 'dompurify';
// NEVER inject untrusted markdown directly into innerHTML
// Always sanitize first
function renderMarkdown(userContent: string): string {
const rawHtml = marked(userContent);
return DOMPurify.sanitize(rawHtml); // XSS protection
}
// React usage
function MarkdownRenderer({ content }: { content: string }) {
return (
<div
dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }}
/>
);
}
remark + unified (Ecosystem)
// remark — AST-based, plugin-rich pipeline
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeSanitize from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';
import rehypeHighlight from 'rehype-highlight';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
// Full pipeline: Markdown → mdast → hast → HTML
const processor = unified()
.use(remarkParse) // Parse Markdown to mdast
.use(remarkGfm) // GitHub Flavored Markdown (tables, strikethrough)
.use(remarkRehype) // Convert mdast to hast (HTML AST)
.use(rehypeSanitize) // Sanitize HTML (safe by default)
.use(rehypeHighlight) // Syntax highlighting
.use(rehypeSlug) // Add IDs to headings
.use(rehypeAutolinkHeadings) // Link headings to themselves
.use(rehypeStringify); // Stringify hast to HTML
const result = await processor.process('# Hello World');
const html = String(result);
// remark — lint Markdown files
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkLint from 'remark-lint';
import remarkLintNoDeadUrls from 'remark-lint-no-dead-urls';
import remarkLintNoMissingBlankLines from 'remark-lint-no-missing-blank-lines';
const file = await unified()
.use(remarkParse)
.use(remarkLint)
.use(remarkLintNoDeadUrls)
.use(remarkLintNoMissingBlankLines)
.process(markdownContent);
// file.messages contains lint warnings/errors
console.log(file.messages);
// remark — custom AST transformation
import { visit } from 'unist-util-visit';
import type { Root } from 'mdast';
// Custom plugin: add "pkgpulse" data attribute to package name links
function remarkPkgPulseLinks() {
return (tree: Root) => {
visit(tree, 'link', (node) => {
if (node.url.includes('pkgpulse.com/compare')) {
node.data = node.data || {};
node.data.hProperties = { 'data-pkgpulse': 'true' };
}
});
};
}
MDX (Markdown + React)
// MDX — Markdown with embedded React components
// Powers: Next.js blog, Docusaurus, Gatsby, Storybook
import { MDXRemote } from 'next-mdx-remote';
import { serialize } from 'next-mdx-remote/serialize';
import remarkGfm from 'remark-gfm';
// In getStaticProps
const mdxSource = await serialize(markdownContent, {
mdxOptions: {
remarkPlugins: [remarkGfm],
},
});
// In component
const components = {
CodeBlock: ({ code, language }) => (
<SyntaxHighlighter language={language}>{code}</SyntaxHighlighter>
),
Callout: ({ children, type }) => (
<div className={`callout callout-${type}`}>{children}</div>
),
};
<MDXRemote {...mdxSource} components={components} />
Performance Comparison
| Library | Parse 10K words | Bundle Size | Security |
|---|---|---|---|
| marked | ~3ms | ~43KB | Manual sanitize needed |
| markdown-it | ~8ms | ~52KB | Sanitized by default |
| remark | ~15ms | ~200KB+ plugins | rehype-sanitize |
When to Choose
| Scenario | Pick |
|---|---|
| Simple Markdown rendering in browser | marked + DOMPurify |
| Comment system, user-generated content | markdown-it (sanitized default) |
| Documentation site (Docusaurus, etc.) | remark/unified |
| MDX (Markdown + React components) | remark (MDX uses it) |
| Custom AST transforms needed | remark (AST-first design) |
| Blog with code highlighting + TOC | markdown-it or remark |
| Speed is the only priority | marked |
Compare Markdown library package health on PkgPulse.
See the live comparison
View marked vs. remark on PkgPulse →