Strateški zapis

Kako sva Mermaid render spravila iz treh sekund pod 200 ms

Majhna arhitekturna sprememba: namesto da za vsak diagram zaženemo Mermaid CLI in nov Chromium, obdržimo en topel browser proces in kličemo mermaid.render neposredno.

Objavljeno

13. maj 2026

Avtor: Simon Zajdela

MermaidPerformanceNestJSPuppeteerDocker

Problem ni bil Mermaid. Problem je bil hladen zagon.

Za diagram rendering sem najprej naredil nekaj zelo običajnega: server sprejme Mermaid kodo, jo zapiše v začasno datoteko in pokliče mermaid-cli. Delovalo je. Bilo je enostavno. Bilo je tudi počasno kot tank, ki ga za vsak strel posebej pripelješ iz skladišča.

Meritev je bila dovolj jasna: približno tri sekunde za en SVG. Za interni ali redko uporabljen endpoint bi bilo to mogoče še sprejemljivo. Za javni diagram service, kjer želiš hitro povratno informacijo, pa ne. Ne zato, ker Mermaid ne zna renderirati hitro, ampak zato, ker je vsak request plačal ceno novega procesa in novega Chromium zagona.

Prva verzija: uradna pot, uradna bolečina

Klasičen pristop je zelo direkten: HTTP request pride na server, server zažene mmdc, mmdc zažene Chromium, Chromium nariše diagram, rezultat se vrne uporabniku, proces pa se konča. To je lepo izolirano, ampak zelo drago, če to delaš za vsak request posebej.

Docker je dodal še svojo zabavo: Chromium sandbox, crashpad, sistemske knjižnice, non-root user in vse male Linux posebnosti, zaradi katerih se človek začne pogajati z apt-get kot z razvajenim printerjem. Ko je zadeva končno delovala, je bila funkcionalna, ne pa elegantna.

Diagram

HTTP request

Old path

Spawn mmdc

Launch Chromium

Render diagram

Return SVG

Optimized path

Reuse warm Chromium

mermaid.render

Razlika ni v Mermaid sintaksi, ampak v življenjskem ciklu rendererja.

Optimizacija: Chromium naj ostane živ

Ključna sprememba je bila preprosta: ne zaganjaj rendererja za vsak request. Ob zagonu NestJS aplikacije odpremo en headless Chromium proces, ustvarimo eno stran, vanjo naložimo Mermaid runtime iz node_modules in inicializiramo Mermaid samo enkrat.

Ko pride request, server ne kliče več CLI-ja. Namesto tega pošlje Mermaid kodo v že odprt browser context in pokliče mermaid.render. Za SVG niti screenshot ni potreben. Browser vrne SVG string, server ga zapakira kot image/svg+xml in pošlje nazaj.

Kaj to pomeni za concurrency

Ker je prva produkcijska verzija uporabljala eno browser page instanco, sem dodal enostavno interno queue. Če prideta dva requesta istočasno, se ne pomešata. Drugi počaka, prvi konča, potem se izvede drugi. Pri renderju pod 200 ms je to čisto sprejemljiv kompromis.

Naslednja stopnja bi bil pool več pageov. En Chromium proces lahko drži več izoliranih page contextov, recimo dva ali štiri. To omogoča vzporedno renderiranje, hkrati pa ne odpira novega browserja za vsak diagram. Ampak prva verzija z queue je bolj mirna, bolj predvidljiva in dovolj hitra za majhen javni service.

Spomin, Docker in produkcijska higiena

Pri long-running Chromium procesu je glavno vprašanje RAM. Zato je smiselno po renderju počistiti DOM, posebej če Mermaid ali browser context sčasoma zadrži elemente. To je majhen cleanup, ki skoraj nič ne stane, lahko pa prepreči počasno nabiranje smeti.

Docker image ostane razumen, če uporabljamo sistemski Chromium in ne prenašamo dodatnega Puppeteer browserja. Pomembno je nastaviti non-root userja, writable tmp direktorije za Chromium profile/crashpad in minimalen nabor sistemskih knjižnic. Ni glamurozno, je pa točno tisti del, ki loči demo od deployane storitve.

Rezultat

Praktični rezultat je bil preskok iz približno treh sekund na manj kot 200 ms za SVG render. CPU se pri osnovnih diagramih skoraj ne premakne, ker najdražji del — zagon browserja — ni več del request patha.

To je ena tistih optimizacij, kjer ni bilo treba dodati kompleksnega cachea, Redis instance ali posebnega worker sistema. Dovolj je bilo premakniti težko inicializacijo iz requesta v startup. Včasih production-ready ne pomeni več infrastrukture, ampak manj ponavljanja neumnosti. Tank naj ostane na igrišču.