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:
| Metric | Client-Side Mermaid | Build-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
pnpm add rehype-mermaid mermaidpnpm add -D playwrightnpx playwright install --with-deps chromiumnpm install rehype-mermaid mermaidnpm install -D playwrightnpx playwright install --with-deps chromiumyarn add rehype-mermaid mermaidyarn add -D playwrightnpx playwright install --with-deps chromiumThree packages, one browser binary:
rehype-mermaid— the rehype plugin that intercepts Mermaid code fencesmermaid— 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).
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:
| Strategy | Output | Best 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
```mermaidgraph 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.
- 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 chromiumvariables: PLAYWRIGHT_BROWSERS_PATH: $CI_PROJECT_DIR/.playwright
cache: paths: - .playwright/
before_script: - npx playwright install --with-deps chromiumTroubleshooting
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.