Tutorial

Build-Time Mermaid Diagrams: Cut 1MB of JavaScript

Stop shipping Mermaid.js to your readers. Render diagrams to SVG at build time with rehype-mermaid and Playwright, cutting ~1MB of client-side JavaScript to zero.

Tin Dang avatar
Tin Dang
Abstract flowchart shapes being transformed through a rendering pipeline into clean organized diagrams

Your Mermaid diagram looks great in the preview. Then you open DevTools and discover you just shipped 1.1MB of JavaScript so a reader could see a static flowchart.

Mermaid.js is a parser, a renderer, and an entire layout engine. It needs all of that to turn text into SVG — but it only needs to do it once. After that, the SVG never changes. There’s no reason to make every reader’s browser repeat that work.

This tutorial shows you how to move Mermaid rendering to build time using rehype-mermaid and Playwright. The result: inline SVG in your HTML, zero client-side JavaScript for diagrams, and pages that load measurably faster.

What You’ll Build

By the end of this post, your Mermaid code fences will render to static SVG at build time. Here’s the pipeline:

Every diagram on this page went through that pipeline. No JavaScript was harmed in the rendering of these SVGs.

The Cost of Client-Side Mermaid

Before we fix the problem, let’s quantify it. Here’s what Mermaid.js adds to your page when loaded client-side:

MetricClient-Side MermaidBuild-Time SVG
JavaScript shipped ~1.1 MB (minified) 0 KB
Parse + render time 200-800ms (per page load) 0ms (done at build)
First Contentful Paint Blocked until JS executes Immediate
Works without JavaScript No Yes
Cumulative Layout Shift Diagram pops in late 0 (SVG in initial HTML)
Build time cost None ~2-5s (one-time, cached)

The tradeoff is clear: a few extra seconds at build time eliminates over a megabyte of JavaScript and hundreds of milliseconds of render-blocking work on every single page load.

Prerequisites

You need three things:

  • Astro 5.x with MDX support (@astrojs/mdx)
  • Node.js 20+ (for Playwright)
  • A terminal and about 10 minutes

Step 1: Install Dependencies

Terminal window
pnpm add rehype-mermaid mermaid
pnpm add -D playwright
npx playwright install --with-deps chromium
Terminal window
npm install rehype-mermaid mermaid
npm install -D playwright
npx playwright install --with-deps chromium
Terminal window
yarn add rehype-mermaid mermaid
yarn add -D playwright
npx playwright install --with-deps chromium

Three packages, one browser binary:

  • rehype-mermaid — the rehype plugin that intercepts Mermaid code fences
  • mermaid — the diagram parser (used at build time only)
  • playwright — headless Chromium that Mermaid needs to measure and layout SVG nodes

Step 2: Configure the Rehype Plugin

Add rehype-mermaid to your Astro config. The critical detail: it must run as a rehype plugin (HTML-level), not a remark plugin (Markdown-level).

astro.config.ts
import rehypeMermaid from 'rehype-mermaid';
export default defineConfig({
// ... other config
markdown: {
rehypePlugins: [
// Other rehype plugins (slug, autolink-headings, etc.)
[rehypeMermaid, {
strategy: 'img-svg',
dark: true,
}],
],
},
});

Strategy Options

The strategy option controls how the SVG is embedded:

StrategyOutputBest For
'inline-svg' Raw <svg> in HTML Maximum control, CSS styling
'img-svg' <img> with data URI SVG Isolation from page styles
'img-png' <img> with data URI PNG Email templates, RSS feeds
'pre-mermaid' Keeps code fence (no render) Client-side fallback

We use img-svg because it isolates the diagram from your page’s CSS while keeping the SVG crisp at any zoom level. If you need to style diagram nodes with your own CSS, switch to inline-svg.

The dark: true flag generates a dark-mode variant automatically.

Step 3: Write Diagrams in MDX

Now write Mermaid diagrams exactly as you would normally — in fenced code blocks with the mermaid language tag. The plugin intercepts them at build time and replaces the code fence with rendered SVG.

Flowcharts

```mermaid
graph TD
A[User Request] --> B{Cached?}
B -->|Yes| C[Return Cache]
B -->|No| D[Query Database]
D --> E[Build Response]
E --> F[Store in Cache]
F --> C
```

Renders to:

Sequence Diagrams

Sequence diagrams are where build-time rendering really shines. These are complex to layout and especially expensive to render client-side.

State Diagrams

Every one of these diagrams is a static SVG in your HTML. View source — there’s no <script> tag loading Mermaid.

Step 4: Set Up CI Caching

Playwright’s Chromium binary is the heaviest dependency. Cache it in CI to avoid re-downloading on every build.

.github/workflows/build.yml
- name: Cache Playwright
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium
.gitlab-ci.yml
variables:
PLAYWRIGHT_BROWSERS_PATH: $CI_PROJECT_DIR/.playwright
cache:
paths:
- .playwright/
before_script:
- npx playwright install --with-deps chromium

Troubleshooting

Diagrams render locally but fail in CI

The most common cause: Playwright’s Chromium isn’t installed in CI. Add npx playwright install --with-deps chromium to your CI script before the build step. The --with-deps flag installs system libraries (libgbm, libnss, etc.) that headless Chromium needs on Linux.

SVG looks different from Mermaid Live Editor

rehype-mermaid uses the Mermaid version in your node_modules, not the version powering the live editor. Pin your Mermaid version and check the Mermaid changelog if diagrams look different after an upgrade.

Dark mode diagrams have wrong colors

With dark: true, rehype-mermaid generates two SVGs wrapped in a <picture> element with prefers-color-scheme media queries. If your site uses a data-theme attribute instead, you’ll need CSS to control visibility. Blogcraft handles this with a @custom-variant dark rule in the global stylesheet.

Build is slow with many diagrams

Each diagram spawns a Playwright page render. For 20+ diagrams, builds can take 30-60 seconds. Two mitigations: enable Astro’s incremental build (only changed pages re-render), and consolidate small diagrams where possible. The build cost is always cheaper than the runtime cost multiplied by every reader.

The Full Picture

Here’s the complete flow from writing a diagram to a reader seeing it — with zero JavaScript involved:

What You Gained

Removing client-side Mermaid from a typical blog post with 3-4 diagrams:

  • 1.1 MB less JavaScript shipped to every reader
  • 0 ms of diagram render-blocking time
  • CLS = 0 because SVGs are in the initial HTML
  • Works without JavaScript — diagrams render even if JS is disabled
  • Faster LCP — no competing with a megabyte of parsing and layout code

The build takes a few seconds longer. Your readers will never notice that. They will notice the page loading instantly.

0