Skip to main content

marked vs remark vs markdown-it: Markdown Parsers in JavaScript (2026)

·PkgPulse Team

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

PackageWeekly DownloadsUsed ByApproach
markdown-it~21MVS Code, DocusaurusRenderer-based
marked~12MMany toolsRenderer-based
remark~8MNext.js, Astro, MDXAST (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 links
  • markdown-it-toc-done-right — Table of contents generation
  • markdown-it-prism / markdown-it-shiki — Syntax highlighting
  • markdown-it-katex / @vscode/markdown-it-katex — LaTeX math
  • markdown-it-container — Custom container blocks (:::: tip ::::)
  • markdown-it-attrs — Add classes and attributes to elements
  • markdown-it-footnote — Footnote support
  • markdown-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

Featuremarkedmarkdown-itremark
CommonMark compliance✅ Best
GFM (tables, etc.)✅ Plugin✅ Plugin
Speed (benchmark)⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Plugin ecosystem⚠️ Small✅ Large✅ Largest
AST access⚠️ Token-level✅ Full AST
MDX support
Syntax highlightingVia rendererVia pluginVia 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.

Compare Markdown parser packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.