Skip to main content

Best Markdown Parsing Libraries for JavaScript in 2026

·PkgPulse Team
0

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

Choosing a Markdown Parser in 2026

Markdown parsing sounds like a commodity problem — every library parses **bold** and # Heading correctly. The real differences emerge at the edges: how each library handles ambiguous CommonMark cases, how they extend Markdown syntax, and whether they prioritize raw speed or transformation flexibility.

For most use cases, the choice simplifies to: rendering user content (prefer markdown-it or marked for security and speed), building a documentation site or blog (prefer remark/unified for the plugin ecosystem), or processing content at scale (prefer marked for throughput).

Security is the most important practical concern. Markdown parsers that output HTML can produce XSS vulnerabilities if untrusted user content is rendered without sanitization. The libraries handle this differently: marked outputs raw HTML and requires explicit sanitization, markdown-it defaults to safe HTML, and remark/unified uses rehype-sanitize in the pipeline.


// 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';
    },
  });

markdown-it's ~20M weekly downloads come substantially from its role as a dependency inside other tools. It's embedded in many CMS platforms, documentation generators, and IDE integrations. Its CommonMark compliance is strict — it passes 100% of the CommonMark spec tests — which means parsed output is consistent and predictable across environments.

The plugin API is intuitive: plugins are functions that extend the md instance, adding new parsing rules or rendering overrides. The ecosystem has ~150 published plugins covering tables-of-contents, anchor links, footnotes, math rendering (KaTeX/MathJax), containers, definition lists, and syntax highlighting integrations. For most documentation use cases, you can compose a complete feature set from existing plugins without writing custom parsing logic.

markdown-it's security model defaults to safe. The html: false default (and it is the default) prevents raw HTML in Markdown source from passing through. Users can't inject <script> tags through markdown-it with default settings. If you need HTML pass-through for trusted content, you enable it explicitly with { html: true }.


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 });
// 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) }}
    />
  );
}

marked's benchmark performance is its defining characteristic: ~3ms for a 10,000-word document, 5x faster than remark. For server-side rendering scenarios where hundreds of Markdown documents are processed per second — blog generation, API documentation, content platforms — marked's throughput advantage is meaningful.

The custom renderer API is marked's flexibility mechanism. You can override the HTML output for any Markdown element — headings, links, images, code blocks — without touching the parsing layer. This is the right level of customization for most projects: change how elements render without reimplementing the parsing logic.

The security caveat is critical: marked outputs raw HTML by default. If you render untrusted user content without sanitization, you have an XSS vulnerability. The correct pattern is marked(content) followed by DOMPurify.sanitize(output) before setting innerHTML. This isn't a quirk — it's by design. marked prioritizes compatibility with all valid HTML, and filtering it would break legitimate use cases.


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';

const file = await unified()
  .use(remarkParse)
  .use(remarkLint)
  .use(remarkLintNoDeadUrls)
  .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' };
      }
    });
  };
}

remark's architecture is fundamentally different from marked and markdown-it: it operates on ASTs (Abstract Syntax Trees) rather than text transformations. The pipeline parses Markdown into mdast (Markdown AST), transforms it through remark plugins, converts it to hast (HTML AST) via remark-rehype, transforms it through rehype plugins, then serializes to HTML.

This AST-first approach enables transformations that text-based parsers can't do easily: renaming all heading IDs, extracting all links for a sitemap generator, replacing package names with links, computing reading time, converting relative URLs to absolute. Any programmatic modification of document structure is straightforward with unist utilities.

remark is what MDX uses internally. MDX (Markdown with JSX) layers React component embedding on top of remark's Markdown parsing. If you're using Next.js's @next/mdx, Velite, Contentlayer, or Docusaurus for content, you're already using remark and can extend it with any remark plugin.

The 300+ plugin count includes both remark and rehype plugins across the unified ecosystem. The practical set most documentation sites need: remark-gfm (GitHub Flavored Markdown), remark-frontmatter, rehype-slug, rehype-autolink-headings, rehype-highlight or rehype-shiki, and rehype-sanitize. That's a one-time setup for a documentation pipeline that handles most use cases.


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} />

MDX is worth treating as a distinct option. Rather than choosing between Markdown libraries, MDX chooses remark for you and adds the ability to embed React components directly in Markdown files. The <CodeBlock> and <Callout> components in the example above are React components rendered inside what looks like a Markdown document.

This is the right tool for documentation-heavy applications (component docs, blog posts that need interactive examples, technical articles with embedded demos) where pure Markdown's limitations become frustrating.


Performance Comparison

LibraryParse 10K wordsBundle SizeSecurity
marked~3ms~43KBManual sanitize needed
markdown-it~8ms~52KBSanitized by default
remark~15ms~200KB+ pluginsrehype-sanitize

When to Choose

ScenarioPick
Simple Markdown rendering in browsermarked + DOMPurify
Comment system, user-generated contentmarkdown-it (sanitized default)
Documentation site (Docusaurus, etc.)remark/unified
MDX (Markdown + React components)remark (MDX uses it)
Custom AST transforms neededremark (AST-first design)
Blog with code highlighting + TOCmarkdown-it or remark
Speed is the only prioritymarked
Linting Markdown in CIremark (remark-lint ecosystem)

Building a Practical Markdown Pipeline

Choosing a Markdown library is rarely just about parsing speed — the decision shapes how you build features like syntax highlighting, table of contents generation, and custom component rendering. Here's how the libraries compare across common pipeline requirements.

Syntax Highlighting

Code block syntax highlighting is one of the most common requirements for developer-facing documentation and blogs. Each library approaches it differently. With remark, rehype-highlight (uses highlight.js) or rehype-shiki (uses Shiki's TextMate grammar system) plug into the unified pipeline. Shiki produces HTML with inline styles that match VS Code themes precisely — it's the standard choice for developer documentation in 2026.

marked integrates highlighting via the highlight option — you pass a function that takes the code and language and returns highlighted HTML. This works with highlight.js or Prism but requires more configuration than remark's plugin system.

markdown-it has an ecosystem of plugins including markdown-it-highlightjs. The integration is similar to marked's callback approach.

Table of Contents Generation

remark-toc generates a table of contents from document headings and inserts it at a configurable location. Since it operates at the AST level, it can extract headings, slug them consistently, and generate internal anchor links in one pass — no second parsing step needed.

For marked and markdown-it, TOC generation typically requires parsing the document twice: once to extract headings and build the TOC structure, then again to render with anchor IDs. This double-pass approach works but adds complexity.

Frontmatter and Metadata Extraction

MDX and documentation sites frequently pair Markdown content with YAML frontmatter for metadata (title, date, tags, author). remark-frontmatter parses frontmatter as part of the unified pipeline, making metadata available alongside the parsed content tree. gray-matter provides frontmatter parsing as a separate pre-processing step that works with any library.

For content pipelines where frontmatter drives routing, navigation, or search indexing, remark's integrated approach means you can extract metadata and transform content in one pipeline invocation rather than sequential parsing steps.

Custom Component Rendering

MDX (Markdown + JSX) is remark's killer application. The @mdx-js/mdx package uses remark and rehype internally, meaning your existing remark plugins work in MDX documents unchanged. You can import and use React components directly in Markdown files:

When you need custom rendering but not the full MDX setup, remark's AST manipulation lets you transform specific node types into custom HTML. The rehype layer provides the final HTML generation, and rehype-react can render rehype ASTs directly to React elements without the JSX compilation step.

Linting Markdown in CI

remark-lint integrates into the remark pipeline to enforce Markdown style rules. Common rules include consistent heading levels (no skipping from H1 to H3), proper list formatting, and consistent code fence styles. The rule set is configurable and can be shared across a monorepo via a shared config package.

Running remark --use remark-lint --frail in CI fails the build when lint rules are violated. This is particularly valuable for documentation repositories where multiple contributors need to follow consistent conventions. Neither marked nor markdown-it have equivalent linting ecosystems — linting Markdown in those cases requires a separate tool like markdownlint-cli.

Performance at Scale

For documentation sites with hundreds of pages, build-time Markdown processing performance matters. marked is consistently the fastest option — its single-pass regex-based parser processes simple documents in microseconds. remark's unified pipeline has meaningful overhead per document (AST creation, plugin traversals) that becomes significant when processing thousands of files sequentially.

The practical mitigation is parallelism: Vite, Next.js content processing, and Contentlayer (which uses remark internally) all process files in parallel worker threads, making the per-file overhead less visible. For sites with under 500 pages, the performance difference between marked and remark is imperceptible. For sites with thousands of pages processed at build time, benchmarking is worthwhile.

Choosing Between Libraries: Common Decision Points

The most common question teams face is whether to start with marked for its simplicity and then migrate to remark for features, or to invest in remark's unified pipeline upfront. The honest answer is that migration from marked to remark is non-trivial — the plugin ecosystems don't overlap, and if you've built custom rendering on top of marked's token hooks, you'll rewrite that logic in rehype. Starting with remark is the right choice if you know you'll need custom transformations, MDX support, or lint tooling. Starting with marked is fine for simple blog rendering where the requirements are unlikely to evolve.

The markdown-it middle ground deserves more credit than it typically receives. Its synchronous API and simple plugin system make it easy to reason about, and it handles edge cases in the CommonMark spec more reliably than older versions of marked. For teams building comment systems, user-generated content rendering, or documentation with a custom look and feel, markdown-it provides a good balance of correctness, security, and configurability without remark's pipeline abstraction overhead.

Security deserves emphasis regardless of library choice. All three libraries can produce XSS vulnerabilities if HTML output is rendered without sanitization in a browser context. marked with html: false disables raw HTML passthrough. markdown-it's default configuration strips most dangerous HTML. DOMPurify, added as a post-processing step, provides defense-in-depth for any library. The one library you should not use for user-generated content without sanitization is a raw CommonMark implementation without explicit HTML stripping.

For applications that need to render Markdown in both server-side and client-side contexts, library choice affects the bundle you ship to the browser. marked at 23KB gzipped is the lightest option. markdown-it at 32KB is heavier but includes security defaults. remark's unified pipeline reaches 60-100KB depending on the plugins you include — meaningful for client-side rendering where bundle size affects page load. Server-side rendering (generating HTML at build or request time) removes this concern entirely.

Compare Markdown library package health on PkgPulse. Related: Best JavaScript Testing Frameworks 2026, Best Documentation Frameworks 2026, and Best JavaScript Package Managers 2026.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.