DEV Community

Cover image for Astro MD vs MDX: Content Collection Build Performance
Sergej Voronko
Sergej Voronko

Posted on Originally published at linuxcore.dev

Astro MD vs MDX: Content Collection Build Performance

Standard Markdown (.md) and MDX (.mdx) process content drastically differently within Astro content collections. The build pipeline parses standard Markdown as raw text data, requiring minimal memory and scaling linearly. The build process compiles MDX files into full JSX components, forcing the Vite bundler to process every .mdx file as a discrete JavaScript module. This architectural difference means MDX processing introduces significant parser overhead and increases build times on low-resource homelab CI runners. Use standard Markdown for static text and reserve MDX strictly for files requiring embedded interactive components.

Astro Markdown vs MDX: Parser Architecture

Standard .md files in Astro bypass the JavaScript module graph entirely. When the content collections API processes a .md file, the parser reads the frontmatter for validation against your defined Zod schema. The standard remark and rehype plugins then transform the body text into HTML. The default Astro pipeline relies on a Rust-powered processor that significantly improves build times. The content remains isolated from the application's core JavaScript bundle.

MDX skips lightweight text processing. When you install the @astrojs/mdx package, the compiler evaluates every .mdx file as a functional component. The parser transforms your Markdown syntax into JSX, injects imports for any Astro, Vue, or React components referenced in the file, and hands the resulting code to Vite. The pipeline then adds these modules to the overall dependency graph.

This fundamental shift alters how the server builds the site. A basic Markdown file maps directly to static HTML output. An MDX file requires the build system to resolve imports, manage potential hydration directives, and pass the output through the Vite build pipeline before generating that same static HTML.

Build Speeds and Parser Overhead

If you run builds on a minimal Proxmox virtual machine or a cheap VPS, the performance delta between .md and .mdx becomes a hard limit. A content collection containing 1,000 standard Markdown files compiles in under a few seconds. Converting those same 1,000 files to MDX pushes the build time into the double digits and routinely triggers out-of-memory kills on runners with under 2GB of RAM.

The performance drop stems from the compilation pipeline. When the build pipeline encounters an MDX file, it does not just render text. The @mdx-js/mdx compiler parses the document into a Markdown Abstract Syntax Tree (AST), applies any configured remark plugins, and converts the output to an HTML AST. The compiler then applies rehype plugins and maps the resulting structure into raw JSX syntax. Finally, the process passes this JSX to the internal bundler to transpile into standard JavaScript. This entire chain executes for every single MDX file. Processing 2,000 blog posts requires 2,000 distinct JavaScript module compilations.

When a homelab CI runner hits the default Node.js heap limit during an Astro build, the pipeline crashes. You can temporarily bypass the crash by forcing Node to use more memory, but this is a patch, not a fix.

# Expanding the heap size for heavy MDX builds
NODE_OPTIONS="--max-old-space-size=4096" npm run build
Enter fullscreen mode Exit fullscreen mode

The proper fix is limiting MDX usage to files that actually require it.

Component Architecture and Hydration

Standard Markdown restricts you to standard HTML tags. You can apply custom styling via CSS, but you cannot embed stateful client-side code directly into the prose.

Many developers assume they need MDX just to add custom CSS classes to alerts or styled callouts. You do not need MDX for basic structural changes. You can write or import custom remark and rehype plugins for standard Markdown that automatically inject HTML div wrappers or CSS classes around standard blockquotes. Standard Markdown in Astro also utilizes Shiki for syntax highlighting by default, meaning you do not need MDX just to render formatted code blocks.

MDX exists specifically to import UI components directly into the content body. This is necessary when an article requires a functional, interactive element, like a live search bar, a dynamic data table fetched from an API, or a React component tracking client-side state.

---
title: "Server Status"
pubDate: 2026-10-27
---

import PingTest from '../../components/PingTest.jsx';

The current server latency is listed below. Click the button to refresh the ping.

<PingTest client:load target="192.168.1.100" />
Enter fullscreen mode Exit fullscreen mode

Because Astro uses an Islands architecture, the MDX file itself still renders to static HTML. Client-side JavaScript only ships to the browser if you explicitly append a client directive like client:load or client:visible to the embedded component. Without the directive, the component renders strictly on the server.

Implementing MDX in Astro Content Collections

If your project requires embedded islands, you must explicitly enable MDX integration and configure your content loader to recognize the extension.

Install the official integration using the Astro CLI:

npx astro add mdx
Enter fullscreen mode Exit fullscreen mode

Optimize the @astrojs/mdx integration in your configuration file. The optimize flag instructs the internal rehype plugin to generate a smaller abstract syntax tree, which cuts down build times and reduces memory consumption for large collections. If you do not require standard Markdown configuration settings to apply to your MDX files, setting extendMarkdownConfig to false ignores inherited project-level Markdown options entirely.

// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';

export default defineConfig({
  integrations: [
    mdx({
      optimize: true,
      extendMarkdownConfig: false
    })
  ]
});
Enter fullscreen mode Exit fullscreen mode

Next, configure your content collection loader to target both file types. Astro uses the glob loader for local file fetching. You can mix .md and .mdx files in the same directory by defining a multi-extension pattern. The [^_] regex ensures Astro ignores files prefixed with an underscore.

// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const docsCollection = defineCollection({
  loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/docs' }),
  schema: z.object({
    title: z.string(),
    pubDate: z.date(),
    draft: z.boolean().default(false)
  })
});

export const collections = { docs: docsCollection };
Enter fullscreen mode Exit fullscreen mode

When rendering the collection in a dynamic route, the render() function parses both file types automatically.

---
// src/pages/docs/[...slug].astro
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const docs = await getCollection('docs');
  return docs.map(entry => ({
    params: { slug: entry.id },
    props: { entry },
  }));
}

const { entry } = Astro.props;
const { Content } = await render(entry);
---

<html>
  <head>
    <title>{entry.data.title}</title>
  </head>
  <body>
    <article>
      <Content />
    </article>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

When to Choose Raw Astro Markdown

Stick to .md files if your content consists only of headings, paragraphs, standard code blocks, and images. The native Astro content collections API handles all frontmatter validation and basic HTML rendering automatically.

Standard Markdown formatting keeps your content portable. If you migrate away from Astro in the future, raw Markdown transfers cleanly to Hugo, Jekyll, or any other static site generator. MDX files littered with framework-specific component imports require manual clean-up and regex replacements to become standard text again.

Integrating a headless CMS like Decap or Ghost is much simpler with standard Markdown. Remote content management systems typically export plain text or JSON. Forcing remote content into an MDX pipeline requires custom loaders and complex hydration strategies that break the native Astro content flow.

Use .mdx exclusively when a specific document requires interactivity that standard Markdown cannot provide. You can safely mix .md and .mdx within the same Astro content collection. Keep the bulk of your site as fast-compiling Markdown, and selectively upgrade individual files to MDX when the content demands custom React, Vue, or Svelte components. This split approach maintains fast build times on limited CI runners while preserving the ability to embed complex UI elements.

What's Next

FAQ

Can I mix Markdown and MDX files in the same Astro content collection?

Yes. The glob loader accepts multiple extensions. Configure the pattern to '*/[^_].{md,mdx}' to process both file types in a single collection.

Does MDX ship JavaScript to the browser by default?

No. MDX compiles to HTML at build time. Client-side JavaScript only ships if you append a client directive like client:load to the embedded framework component.

Why are my Astro MDX builds failing with out-of-memory errors?

Vite bundles every MDX file as a discrete JavaScript module. Large MDX collections overwhelm CI runners with limited RAM. Enable the optimize flag in the MDX integration or increase runner memory.


Originally published at linuxcore.dev.

Top comments (0)