Skip to content
Ferrigura

Ferrigura / Comparisons

Choosing a renderer

How ferrigura stands next to other renderers of Mermaid's syntax. Each project has a page of its own: what it is, how it differs from ferrigura, and what ferrigura can learn from it. This page holds the overview and the measurements. Facts are from each project's documentation and sources at the version named, as of 2026-10-24; measured values say how they were measured.

ProjectWhat it is
MermaidThe original, and the reference ferrigura is compared against
PeleAn independent, fast renderer of Mermaid's syntax, for Obsidian
mermanA headless Rust implementation of Mermaid, aimed at its output, with many outputs and bindings
mermaid-rs-rendererA fast Rust renderer of Mermaid's syntax with a layout of its own

Overview

ferriguraMermaidPelemermanmermaid-rs-renderer
Version0.2.0 (2026-10-22)12.2.01.9.00.9.2 (2026-10-21), after Mermaid 12.1.00.3.1 (2026-07-06)
BySebastian Softwarethe mermaid-js projectkepano, for ObsidianLatias94Jeremy Huang
LicenseMIT; ported parts under EPL-2.0 (ELK), Apache-2.0 and BSD-3-Clause (llvm-libc, V8, Skia)MITMITMIT OR Apache-2.0; EPL-2.0 for its ELK portMIT
LanguageRustTypeScriptTypeScriptRustRust
Runs inA library, a CLI and a native Node addon; no browser, no WebAssemblyA browser, or a headless one on a serverAny JavaScript runtime, browser or serverA library, a CLI, WebAssembly, Node, Python and moreA library and a CLI
Needs a DOMNoYesNoNoNo
APISynchronous, returns the SVGAsynchronous, draws into the pageSynchronous, returns the SVGSynchronous, returns the SVG or another formatSynchronous, returns the SVG
GoalMermaid's output(the reference)Mermaid's syntax, its own drawingMermaid's output, without pixel identityMermaid's syntax, its own drawing
Output against MermaidThe same geometry within 0.05 px on every comparable fixture(the reference)Its own layout and styleCompared as SVG DOM with mermaid-cli's outputIts own layout and style
Diagram families33 of 333333 (those of Mermaid 12.1.0)37 by its count, the railroad dialects one by one23
LayoutMermaid's engines, ported: ELK Layered, cose-bilkent, fcose, the tidy treeELK (the default since 12.0), cose-bilkent, fcose, and moreIts own: layered, compound, lanesMermaid's engines, ported: dagre, ELK Layered, cose-bilkent, fcoseIts own layered layout
Text measurementThe bundled faces, shaped with HarfRust and rounded as Chromium roundsThe DOM: getBBox(), getComputedTextLength()The canvas's measureText; an Arial width table without oneDeterministic heuristics; font files as an optionThe fonts installed on the machine
FontsEmbedded in the SVG as a subset of the glyphs usedThe page'sThe page'sThe viewer'sThe viewer's
LabelsSVG textHTML in foreignObject by defaultSVG textHTML in foreignObject by default; SVG text as an optionSVG text
ColorsCSS custom properties with attribute fallbacks; Mermaid's redux themesThemes and themeVariablesCSS custom propertiesMermaid's themes and themeVariablesIts own modern theme by default; Mermaid's themes
Output formatsSVGSVG; PNG and PDF through mermaid-cliSVGSVG, PNG, JPEG, PDF, ASCIISVG, PNG
Narrow screensScales the diagramScales the diagramAdapts the layoutNot checkedNot checked
Size, gzip (raw)7.2 MB (17.9 MB), the native addon, fonts included1.57 MB (5.5 MB), mermaid.min.js261 kB (702 kB), pele.jsNot measured hereNot measured here
Direct dependencies14 crates230Not counted here9 crates (12 with its default CLI and PNG)
Render time, median0.70 ms, through a reused renderer7.8 ms0.05 msNot measured hereNot measured here

The sizes are not alike: ferrigura's is a native library for one platform, with its text engine and 1.4 MB of fonts, where Mermaid and Pele are JavaScript that the browser brings the rest to. merman and mermaid-rs-renderer are not measured here yet: their facts are from their documentation and sources, and the benchmark below runs on JavaScript and the Node addon.

Render time

Measured with tools/port/compare-bench.ts on the 215 pr-check fixtures of the 25 types all three render: each time is the median of 30 renders of one fixture (10 for Mermaid) after a warm-up. ferrigura ran through its Node addon (release build) in two ways: render, which takes the configuration with each call, and a Renderer made once for the configuration and reused for every render. Pele ran in Node (with its width table) and in Chromium (with the canvas), Mermaid in Chromium with its defaults. Every fixture rendered with all three. Machine: Apple M1 Ultra, macOS 27.0.1, Node 24.22.0, Chromium 151.0.7922.34. Run of 2026-10-24, with ferrigura 0.2.0, Pele 1.9.0 and Mermaid 12.2.0; two runs earlier that night, under the load of a backup, gave ferrigura medians up to 0.84 ms and left Pele and Mermaid where they are.

Per diagram, over all 215ferriguraferrigura (Renderer)Pele (Node)Pele (Chromium)Mermaid (Chromium)
Median0.70 ms0.70 ms0.05 ms< 0.1 ms¹7.75 ms
90th percentile2.01 ms2.01 ms0.15 ms0.10 ms33.3 ms
Slowest34.2 ms36.6 ms0.61 ms0.60 ms93.3 ms

The two ferrigura columns are the same now: the shorthand's cache of the resolved configuration costs no more than a Renderer made once (on 2026-10-18, render took 0.73 ms for a fixture without configuration, against 0.34 ms for the Renderer). Of the 215 fixtures, 103 carry a directive or a configuration block, which each render merges on top of the resolved configuration, as the decision keeps: 0.93 ms in the median. The other 112 use the resolved configuration as it is: 0.37 ms. Against the run of 2026-10-18, before the P3 types, the swimlanes and the emoji face came in, the median rose from 0.66 to 0.70 ms and the 90th percentile from 1.61 to 2.01 ms, the state diagrams the most (1.60 to 2.13 ms); what costs the difference is not measured.

¹ Chromium's timer resolution is 0.1 ms on a page that is not cross-origin isolated, which is below what Pele needs.

The median per type, in milliseconds:

TypeFixturesferriguraferrigura (Renderer)Pele (Node)Mermaid
flowchart111.001.010.1221.50
sequence100.790.780.0713.48
class91.221.220.0732.05
state102.152.130.1131.80
state v2101.571.570.0822.90
ER100.880.880.0716.80
gantt110.410.410.104.55
timeline101.091.100.035.30
gitGraph100.480.480.047.92
journey110.740.730.046.30
pie100.380.380.022.60
info20.060.060.011.73
mindmap41.061.050.0422.18
block100.360.360.0419.80
packet50.290.290.022.55
xychart100.820.820.057.18
sankey30.800.800.117.75
quadrantChart100.270.270.035.05
requirement21.321.300.1022.57
C4100.660.660.068.85
kanban100.360.360.025.37
architecture100.950.990.0818.60
treemap100.490.480.054.28
ishikawa110.380.380.014.10
eventmodeling60.240.240.043.70

The work is not the same either: ferrigura embeds a font subset and reproduces Mermaid's layout, Mermaid lays out and measures in the page, and Pele in Node estimates text widths and draws a simpler layout.

Where the time goes

A CPU profile of each shows where the time goes (profiles of 2026-10-17 and 2026-10-18). In Mermaid, and in ferrigura before step 2 of decision 0008, much of the time went to the configuration, not to drawing:

  • Mermaid (Chromium's profiler, 475 renders of the pr-check fixtures of ten types): 57 percent of the time is in assignWithDepth, the deep merge that getConfig() runs to hand out a fresh copy of the whole configuration, theme variables included, every time a renderer, shape or label asks for it. Then ELK and the other layouts (17 percent), DOMPurify (10), style and layout work of the browser (6), and the DOM measurement (getBBox() and the like, with the layout it forces: 4). So measuring text with a canvas, as Pele does, would save Mermaid a few percent; handing out the configuration without a deep copy per call would save more than half.
  • ferrigura (macOS sample, 12 seconds of small pie renders through one Renderer, 0.13 ms each, with no directive): the configuration no longer shows in the profile. Before step 2, 63 percent of the time of such a render was in prepare, which merged the layers (effective) and read the typed view (typed) on every call. The render now takes the resolved configuration as it is, and about half of its samples are in the text crate, embedding the subset of the bundled faces that the labels use (the skera subsetter), which each SVG needs to be self-contained. A text with a directive or a configuration block still merges it on top in each render. In a profile of one such flowchart, prepare took 47 percent of the render, most of it in the typed view of the merged configuration (typed, the lenient reader and serde) and in the layers (effective). That is the next cost to take, with the typed sections of decision 0008, step 3. Step 3 is done since 2026-10-19 (issue #4); its effect is not measured here.
  • Pele was not profiled here. It has no DOM, no theme to compute and no configuration of that size to merge (its page), which is where the other two spend their time.

Adding a comparison

Add a page named after the project with the sections of the others (what it is, how it differs from ferrigura, what ferrigura can learn from it), a row in the table at the top and a column in the overview, and run the benchmark with it when it can render the same fixtures. Name the version and the date of every fact.