Mermaid 布局引擎十年:dagre 冻结、ELK 上位,换主人的从来不是算法
目录

「流程图节点超过 100 就卡,因为默认的 Dagre 布局算法有局限,换成 ELK 就好了。」
这句话在中文技术社区被转载过很多次,听起来也合理——谁都知道 Mermaid 画复杂流程图的时候会慢。
但这句话里至少有一件事早就过期了:Mermaid 的默认布局已经不是 dagre。我给自己那个 Mermaid 编辑器挑引擎的时候,把 dagre、ELK、elkjs 和 mermaid 的源码都拉到本地翻了一遍,翻完的发现是:这十年里推动布局换主人的,几乎从来不是算法。
先纠正三个流行说法
第一,flowchart 的默认布局已经是 ELK 了。 从 mermaid 12 开始,ELK 随包发布并成为默认,官方把它当成一次 breaking change 宣布:
ELK is now bundled with mermaid and is the default layout algorithm. Flowchart, state, class, entity-relationship, requirement, use-case diagrams that do not specify a
layoutare now laid out by ELK instead of dagre.Existing flowcharts, state and class diagrams will re-lay out and recolour; add
layout: dagre,theme: defaultandlook: classicto your config to keep the old look.
在 mermaid 12 的源码里能直接看到这个落点:配置 schema 里 layout 的默认值写着 elk,而渲染入口那句 LAST_RESORT_LAYOUT = 'dagre' 已经退化成「找不到注册布局时的兜底」——从主角变成了备胎。
第二,mermaid 现在依赖的那个「dagre」不是 dagre。 原名包最后一版停在 2019 年 12 月,之后再没动过(仓库连归档都没归档,只是没人维护了;官方 README 索性把维护让给了另一个 scoped 包)。mermaid 用的是 dagre-d3-es——一位叫 tbo47 的开发者的个人 fork,它自己的 README 写着「a fork of dagre-d3 using the more modern ES6 javascript syntax」。
所以现状是:一个 2019 年冻结的项目,靠别人的个人移植,继续在世界上最大的图表库里当兜底布局。
第三,mindmap 用的 cose-bilkent 跟 ELK 没有任何关系。 官方文档特意点了一句:
Mindmaps are the one diagram type not laid out with ELK by default; they use cose-bilkent unless you ask for something else.
但 cose-bilkent 来自 Cytoscape.js 生态(Bilkent 大学的 CoSE 算法),不是 ELK 的某个布局。它最后一次发版也在 2019 年 9 月——跟 dagre 冻结几乎是同一时间。
这三条加起来的意思是:「Mermaid 的布局」这十年换过三次主人,而绝大多数中文教程还停在第一代。
十年时间线
图里绿色是布局算法本身的演进,橙红是许可与体积造成的位移。一眼就能看出橙色比绿色多,而且每一次橙色事件都直接改变了默认布局的归属。
值得单独说的是 2023 到 2024 那一段。flowchart-elk 最早是内置的懒加载图类型,用户写 flowchart-elk 关键字就能用,那还是 mermaid 9.4 的事。后来它被拆成独立包——而拆包的理由不是性能,是许可。
两条暗线:许可与体积
只看时间线会以为这是一部算法演进史,实际上它更像一部依赖管理史:每次布局变动,动机都是许可或者体积,跟布局质量关系不大。
许可这条线。 mermaid 是 MIT,ELK 是 EPL-2.0——weak copyleft,静态链接也要履行义务。2023 年那个要求拆包的 issue 里,用户的原话是:
since elkjs has an Eclipse Public License 2.0 (which is a copyleft license), this forces us (and any commercial closed source users) to use an older version of mermaid-js.
拆出来的包只发过一个 rc 版就再没动过,一年多后被删掉、改名成 @mermaid-js/layout-elk。ELK 那边直到 2026 年春天才给 EPL-2.0 补上一条 GPL-3.0-or-later 二级许可,而 mermaid 至今锁的还是那个旧版 elkjs。于是就有了今年 10 月那条 issue:Forgejo 说 elkjs 的许可跟自己不兼容,整个项目只能停在 mermaid 11。
顺带一个诚实的注脚:那条 issue 里把许可证写成了 EUPL,而我本地 elkjs 和 ELK 的 LICENSE 文件首行都是 Eclipse Public License 2.0。两边措辞不一致,我不替谁断言——但结论不受影响:许可没跟上,下游就升不了级。
体积这条线。 @mermaid-js/tiny 用 esbuild 把 ELK 整个从构建里排除掉,体积从 3MB 降到 1.1MB。代价是可以接受的:官方文档说,在 tiny 构建里请求 ELK 布局的图会退回 dagre,照样能渲染。
于是同一条 EPL-2.0,在浏览器里被当成体积问题解掉了;但在桌面 App 里解不掉——这是我自己踩到的:
关掉 layout-elk 这个 cfg 门控特性 ⇒class / flowchart / er / mindmap / state / usecase / requirement七族的布局从 ELK 退回 dagre ⇒ 几何全变 ⇒ 撞穿大量像素级门禁那叫产品回归,不叫合规选项。所以区别不在「EPL 麻不麻烦」,而在布局质量对你来说是不是一等公民:对文档站它是可选优化,对一个以画图为主业的工具,它就是产品本身。
算法层:两家其实走同一条流水线
抛开工程史,布局算法本身没那么神秘。分层图布局是 1981 年 Sugiyama 那篇论文定下来的框架,ELK 官方文档的说法是:
The layer-based method was introduced by Sugiyama, Tagawa and Toda in 1981. It emphasizes the direction of edges by pointing as many edges as possible into the same direction. The nodes are arranged in layers … and then reordered such that the number of edge crossings is minimized. Afterwards, concrete coordinates are computed for the nodes and edge bend points.
dagre 和 ELK 的 layered 都严格走这条流水线,把两边源码摊开对照,几乎一一对应:
dagre 的执行顺序在 lib/layout.ts 里原样可读:acyclic.run → nestingGraph.run → rank → normalizeRanks → normalize → parentDummyChains → addBorderSegments → order → position;ELK 更省事,直接拿阶段给目录命名:p1cycles / p2layers / p3order / p4nodes / p5edges。
连细节都撞车:dagre 排层用 network simplex,横坐标用 Brandes–Köpf(lib/position/bk.ts 的文件头就写着这篇论文);ELK 的 p4nodes 默认也是 Network Simplex,而且同样带了一份 BK 实现。所以「ELK 算法比 dagre 先进」这句话,在前四步基本站不住。
真正的分水岭在第五步。ELK 把边路由当成独立阶段(可选 Orthogonal / Polyline / Spline,还能挂 libavoid 只重排边、完全不动节点),而 dagre 走完坐标就结束了,边的形状只是收尾时的副产品。
这一个差别解释了不少现象:为什么同一份 .mmd 换个布局引擎会整图重排(不只是边变了),为什么 ELK 有一大堆可调的间距和路由参数而 dagre 只有 nodeSpacing / rankSpacing。顺便说,ELK 的算法家族比「layered」大得多,mermaid 12 里开箱就能用的有六个变体:
layout 取值 | 类别 | 一句话 |
|---|---|---|
elk | Layered | 分层,即 elk.layered,默认 |
elk.mrtree | Tree | 先求生成树,再按父子层级排 |
elk.stress | Force | stress majorization,让欧氏距离贴合图论距离 |
elk.force | Force | Eades 与 Fruchterman–Reingold 两种模型 |
elk.box | Packing | 无边的孤立盒子怎么排 |
elk.rectpacking | Packing | 矩形装箱 + 收缩 |
elk.sporeOverlap | 后处理 | 去重叠(Nachmanson 等人的 growing-a-tree) |
dagre | Layered | 退回旧行为 |
cose-bilkent | Force | mindmap 默认,来自 Cytoscape.js |
tidy-tree | Tree | 需要额外装 @mermaid-js/layout-tidy-tree |
顺便辟个谣:libavoid、vmson、organic 都不是 mermaid 里能填的 layout 值——libavoid 在 ELK 那边是边路由模块,在 mermaid 仓库里只出现在拼写词表和泳道图的注释里。
布局质量怎么量:绕行比
前面讲的都是谁在做布局、怎么做。还剩一个更难回答的问题:做出来的布局好不好,能不能量化。这个维度几乎没有客观说法,我的做法是把它压成一个可复查的比值。
一条边的绕行比 = 折线实际长度 / 两端点直线距离。上面那张图里,Decide → Browse 这条回边因为标签压在弦这一侧,只能绕上去:弦 150、折线 259,读数 1.72。同一把尺量 elkjs 的等价拓扑,它把标签放到弦的另一侧,读数是 1.47。
也就是说我们比参照绕了 17%。这个数字我没有写成「与参照持平」——它是一个已知的、还没修的差距,照抄上游的取舍,就得连上游的缺陷一起认下来。
阈值怎么钉,是这套做法里我最想推荐的一段:
其余 7 张图最大读数 1.43那张图的某条边曾经 1.90,修完之后 1.60 —— 1.60 是它的结构地板全局阈值 = 1.60 + 0.1 = 1.7 (仍然咬得住回退到 1.90 的回归)这张图的 override = 1.72 + 0.08 = 1.8阈值不是按当前读数拍的。它锚在「结构地板 + 一点余量」上,同时保留对历史回归的杀伤力。想放宽就得给出新的活性证据,修好了就把 override 删掉,而不是把全局阈值调大、把回归藏进去。
除了绕行比,还有折点数、净距地板、穿节点、互压、孤立、非正交、标签截断这一组指标,每一项都是一个上界。它们有个共同的性质:换布局引擎会让它们全部红一遍——这才是「换引擎」在真实项目里的成本,而不是 npm 上换个包那么简单。
三条可以带走的结论
1. 结论要写成机制,不要写成排名。 「ELK 比 dagre 好」会在下一个版本失效(事实上「dagre 是默认」这句已经失效很久了),而「ELK 把边路由当独立阶段、dagre 没有」会一直成立,还能推出前者的适用边界。我自己写文档时也栽过:官网上曾经印着「34 族里有 12 族支持结构编辑、30 族支持改名」,两句写下时都对,同一个 release 里两个页面就漂到了 29 和 30——最后只能把计数全删掉,让产品自己当权威。
2. 布局漂移是 breaking change,就按 breaking change 对待。 mermaid 12 自己就是这么宣布的,还给了逃生阀:layout: dagre 加 theme: default 加 look: classic 三件套保持旧观感。它随后挨的投诉也值得看:重复的 subgraph id 会算出 NaN 几何、渲染中途上屏导致闪烁、ELK 的间距和回边路由根本配不了。默认布局一变,会打穿所有下游的截图、测试和文档。
3. 许可是选型的一等公民,不是收尾的合规作业。 EPL-2.0 在 Mermaid 布局史上造成的位移,比任何一个算法改进都大:它逼出了拆包、决定了 tiny 构建里装什么、现在正卡着 Forgejo 的升级。挑依赖的时候不看许可证,最后你会在「履行义务」和「产品回归」之间二选一——而那时候已经没有第三个选项了。
尾声
我为了做一个 macOS 原生的 Mermaid 编辑器,把上面这些全走了一遍:34 个图族的布局与渲染脱离浏览器和 WebView 重画,Swift 加 CoreGraphics 直接落笔,布局交给 Rust 引擎 merman。
工具叫 Mernyx,官网 mernyx.com,免费可下载。如果你也希望在画布上拖一个节点时,Mermaid 源码跟着变、而不是被导出成某种私有格式,可以去看看。
附:本文的说法去哪核对
版本号与日期取自 mermaid 官方 release notes、英文文档和 npm registry;实测部分来自我自己仓库里的质量门禁源码。几个关键锚点:
| 想核对的东西 | 去哪看 |
|---|---|
| ELK 成为默认 | mermaid 12.0.0 的 release notes(2026 年 9 月),PR #8155 |
layout 选项与外部布局注册 | mermaid 11.0.0 的 release notes(2024 年 8 月) |
| 当前支持的 layout 取值 | https://mermaid.js.org/config/layouts.html |
| tiny 构建的体积数字 | PR #4734,标题就写着 69.7% size reduction |
| dagre 冻结 | npm registry 上 dagre 的最后一版发布于 2019 年 12 月 |
| elkjs 的许可与版本 | npm registry 上 elkjs;ELK 仓库 issue #312 / #373 |
| 绕行比阈值的推导 | 本地 SceneQualityBudgets.swift 里 DetourBudget 的注释 |
示意图都是我自己手绘的 SVG。其中「绕行比」那张标了「示意,非实测坐标」——形状是为了解释这个读数是怎么来的,它本身不是读数。