Rendering Mermaid diagrams without a browser


Part 2 of 2 in the series Rendering Mermaid Diagrams in PayloadCMS Posts. Full series ↓

I added Mermaid diagram support to this site's editor, and the first version worked but bugged me. It shipped the whole Mermaid library to the browser, roughly half a megabyte, and drew each diagram after the page hydrated. So a reader would load the post, see a blank gap where the diagram should be, and then watch it pop in a beat later. Every other kind of content here renders to finished HTML on the server before it ever reaches a browser. The diagrams were the odd one out.

How the rest of the site works

Code blocks are the closest cousin. When I publish a post, a server component runs the code through Shiki and injects the highlighted HTML. The browser gets colored markup and nothing else: no highlighter, no theme files, no work to do. The pages themselves are generated ahead of time, so "at publish" and "for every visitor" are the same moment. That model is the whole reason the site feels quick.

I wanted diagrams to behave the same way. Render the SVG when the page is built, drop it into the HTML, and send zero JavaScript for it.

The catch

Mermaid is built for the browser, and not by accident. To size a box around a label, it puts the text in the DOM and asks the browser how wide it came out. Take away the browser and you take away the one thing it uses to lay diagrams out. Running it in Node without a real DOM is the actual problem, and it's why "just render it on the server" isn't a one-line change.

I looked at four ways around it:

  • A pure-JavaScript renderer that parses the diagram and emits SVG on its own, no DOM anywhere. Cleanest fit if it covers the diagram types I use.
  • jsdom plus Mermaid. No browser process, but jsdom doesn't do real layout, so the text measurements come back wrong and complicated diagrams misfit.
  • Puppeteer, which drives a real headless Chrome. The most accurate option and the heaviest. Shipping Chromium into a build for a personal blog felt like bringing a forklift to carry a bag of groceries.
  • An external service like Kroki: POST the diagram text, get SVG back. It works, but now the build depends on a network call to something I have to keep running.

What I landed on

The pure-JavaScript route won, through a library called beautiful-mermaid. It parses the diagram to a tree, lays it out with a JavaScript port of the ELK layout engine, and sizes text from its own width tables instead of asking a browser. There's no DOM, no headless Chrome, and no service sitting behind it that I have to keep alive. It covers the diagram types I actually write (flowcharts, sequence, state, class), and it runs synchronously, so the server component stays boring.

Two things made it a good fit for a statically built site. Rendering the same diagram twice gives byte-for-byte identical output, which is what you want when pages are generated ahead of time. And the whole thing runs at build, so the library's weight never reaches a reader. The component ended up looking almost exactly like the code-block one:

The parts that fought back

It wasn't a clean drop-in. A few things needed handling.

The library bakes a Google Fonts @import into every SVG. That means a static file, served from my own domain, would quietly reach out to Google for a font on every view. I didn't want a third-party request hiding inside otherwise self-contained markup, so I strip that line out after rendering and let the system font stack take over. The diagrams look the same and phone home to no one.

I also needed a plan for diagrams the library can't draw. Ask it to render a Gantt chart or a pie chart and it throws. Rather than let a bad diagram blank out the block, a failed render falls back to showing the raw source in a <pre>, the same defensive move the code blocks make for an unknown language. A reader still gets something legible.

The one that caught me off guard was on mobile. Wide diagrams were spilling off the right edge of the screen and dragging the whole page sideways. The cause wasn't the SVG at all. The diagram sat in a flex container, and a flex child defaults to refusing to shrink below its natural width, which quietly cancelled the max-width that was supposed to scale it down. Swapping the container back to a plain block fixed it, and diagrams now scale to fit the column.

There was even a snag in taking the before-and-after screenshots. My usual tool for turning an SVG into a PNG couldn't read the diagram's colors, because they're defined with newer CSS it doesn't understand, and it rendered every shape solid black. I ended up driving a real headless Chrome to get an honest picture, which was a funny thing to hit right in the middle of a change about not needing a browser.

Where it ended up

Diagrams now render to SVG when a post is built and land in the HTML as finished markup. No diagram library ships to the browser, there's nothing to hydrate, and the picture is there the instant the page paints instead of flashing in after. It works with JavaScript turned off, it's in the static HTML for search engines to read, and it finally matches how the rest of the site treats content. A post with two diagrams now serves both inside the page, with no diagram code and no font request going out.

What I'm watching next

There's a cost I haven't measured yet. Because the pages are built ahead of time, every deploy re-renders every diagram from scratch, even for posts that didn't change. Right now that's almost certainly nothing: the renderer is fast and there are only a handful of diagrams. But it's the kind of thing that stays invisible until there are a hundred posts and the build starts to drag. So I've written it down as a follow-up: measure what diagram rendering actually adds to a build, and decide whether a small cache keyed by the diagram's contents is worth the bother. Since the output is deterministic, an unchanged diagram could skip straight to its saved SVG. Until the numbers say otherwise, I'm leaving it alone.

Comments

No comments yet. Be the first to comment.

Leave a comment