X6 框选拖拽性能内幕:从 issue 4823 到开源内核(系列 3 篇)之一

X6 框选 200 个节点就卡死?翻完 1100 行源码,病灶是四笔账

系列第 1 篇 · 病灶篇。读完你能:沿着一次 mousemove 的调用链,把"框选拖拽卡顿"拆成四笔可数的账,并说出为什么本地测 20 个节点永远测不出来。

系列目录:README · 下一篇:X6 修框选拖拽卡顿只动了三处,每一处都值得抄

一个真实的场景

电力一次接线图、SCADA 画面、工厂布局图这类工业前端,画布上几百上千个设备符号是常态。用户框选一片区域,按住拖走------20 个节点丝般顺滑,200 个节点开始发粘,500 个节点几乎不可用。

这不是猜想,是 AntV X6 社区真实 issue(issue 4823)的形态。我最初以为答案会藏在"节点渲染"里------图形多了当然慢。但把修复前后的 selection 插件源码(@antv/x6-plugin-selection@2.1.6 vs @antv/x6@3.1.7)并排读完之后,发现真正的病灶离渲染很远,就在最不起眼的一行事件处理里。

这篇文章讲清楚病在哪。怎么修,下一篇讲。

第一步:校准心智模型------这是两个手势,不是一个

排查前我先复述了自己的理解,结果发现有一处根本性偏差:我把"框选"和"拖走"揉成了一段流程。实际上代码里它们是两个独立手势,用一个字段区分:

ts 复制代码
action: 'selecting'     // 手势A:框选(空白处按下,拖出橡皮筋)
action: 'translating'   // 手势B:拖走(在选中框上按下,移动整群节点)

配套的鼠标三连也容易记错名字------不是 mouseover,而是:

事件 触发频率 在这个流程里的角色
mousedown 1 次/手势 记起点
mousemove 60~120+ 次/秒 干活的主力,也是病灶所在
mouseup 1 次/手势 收尾结账

关键区分:

  • 手势 A(框选) :mousemove 只负责把橡皮筋画大,节点不动 ;"哪些节点在框里"是 mouseup 时一次性 hit test 算出来的。这一步 O(n) 扫一遍完全便宜,不是性能问题。
  • 手势 B(拖走) :mousemove 里把整群选中节点全部移动 。频率高、单次工作量随节点数线性涨------四笔账全长在这条链上。

先把两个手势分清,后面的账才算得明白。

旧版的一次 mousemove:五层嵌套,逐层看

修复前的版本(2.1.6)里,每次 mousemove 的完整调用链是这样的:

text 复制代码
document 事件 mousemove
└→ adjustSelection(evt)                          // 事件入口
   └→ case 'translating':                        // 手势B分支
      └→ updateSelectedNodesPosition(offset)     // 拿到位移
         └→ translateSelectedNodes(dx, dy)        // ★ 真正干活
            ├→ 遍历每个选中 cell
            ├→ 重算子孙排除表
            ├→ ★ getConnectedEdges(cell)          // 每个节点查一次连接边
            └→ edge.translate(...)                // 移动相关边
         └→ updateSelectionBoxes()               // 刷高亮框

代码本体(2.1.6,updateSelectedNodesPosition)直白得惊人:

ts 复制代码
protected updateSelectedNodesPosition(offset) {
  const { dx, dy } = offset
  if (dx || dy) {
    this.translateSelectedNodes(dx, dy)   // ← 立刻全量执行,没有任何"攒一攒"
    // ...随后同步刷新高亮框
  }
}

没有 rAF、没有缓存、没有延迟------事件到达即全量执行 。这是最自然的写法,本地跑 20 个节点屁事没有,X6 维护者最初也是这么写的。问题在于这笔账会随两个变量相乘放大:选中节点数 × 事件频率。

四笔账:卡顿的完整分解

把上面的调用链按开销类型拆开,一次 mousemove(假设选中 200 节点)的账单是:

账 1:模型层全量位移

translateSelectedNodes 对每个选中节点调 cell.translate()。200 个节点 = 200 次坐标写入,且每次 translate 都会派发事件(node:change:position 等),选中集合上还挂着监听器。

量级:O(n) 模型写 × 每事件。

账 2:逐节点现场查边(最隐蔽的一笔)

同一函数里,每个节点都要问一次图:

ts 复制代码
this.graph.model.getConnectedEdges(cell)   // "你牵着哪些边?"
  .forEach((edge) => { ... edge.translate(...) })

getConnectedEdges 要在模型的边集合里扫描匹配。200 节点 = 每帧 200 次扫描 ,而且------这是关键------从按下鼠标的那一刻到松手,答案一次都没变过。选中名单没变、接线关系没变,这道题每帧重答一遍,纯属浪费。

量级:O(n × E) 现场查询,零缓存。

账 3:高亮框 DOM 重建

每个选中节点外面有一个高亮框(DOM div)。旧版刷框的实现是销毁重建:

ts 复制代码
confirmUpdate() {
  for (const box of this.$boxes) {
    Dom.remove(box)                     // 撕掉旧框
    // getBBox 重新量位置
    document.createElement('div')       // 重新裁一张
    // 设样式、append
  }
}

200 个节点 = 每帧删 200 个 div、建 200 个 div,每个还要问一次几何。DOM 删增是浏览器最贵的操作之一(引发 layout 树大改),而且触发它的入口不止一条。

量级:O(n) DOM 创建销毁 × 每帧。

账 4:布局级样式写入

走不到重建分支的路径也好不到哪去:逐个读改框和容器的 left/top。改布局属性会触发布局流水线(layout → paint),而不是走 GPU 合成层。

量级:O(n) 样式读写 × 每帧,每次可能触发强制同步计算。

账单汇总

账目 旧版每次 mousemove 放大系数
1 模型位移 O(n) 写入 + 事件派发 × 事件数
2 查连接边 O(n × E) 扫描,无缓存 × 事件数
3 高亮框 O(n) DOM 删建 × 事件数
4 样式写入 O(n) left/top,触发布局 × 事件数

一秒 60~120 次事件 × 200 节点 × 四笔账------单帧预算 16ms 被轻松打爆,这就是你手指下面那层"粘"。

一句话:卡的根源不是"节点多",是"高频事件 × 每次全量"。

为什么这类 bug 本地永远测不出来

值得单独说一下,因为它是这类性能问题最阴的地方:

慢的写法和快的写法,在小数据量下没有区别。 20 个节点时,四笔账加起来可能只有零点几毫秒,两种实现都流畅。账单是 n × 事件频率 的乘积------n 小的时候乘不出来。

所以它只在客户的真实数据(几百上千设备)下爆发。等你复现的时候,问题已经到用户手里了。这类 bug 的防线不是"跑一遍 demo",而是读代码时就按公式估算:把 O(n) 的工作放进高频事件处理器之前,先问一句"n 到 1000 时这笔账是多少"。

病灶确认清单

到这一步,四条结论可以直接带进下一篇:

  1. ✅ 框选(selecting)不是问题------命中测试只在 mouseup 做一次。
  2. ✅ 病灶全在拖走(translating)的 mousemove 链上。
  3. ✅ 四笔账:模型全量位移、逐节点查边、高亮框 DOM 重建、布局级写入。
  4. ✅ 修复的思考方向必然是回答三个问题------事件能不能少执行?(合帧)单次计算能不能少算?(缓存)DOM 能不能不动?(冻结)

这三个问题就是下一篇的三刀。


系列导航 :目录 · 本文为第 1 篇 · 下一篇:X6 修框选拖拽卡顿只动了三处,每一处都值得抄

如果这篇帮你定位了问题,欢迎在评论区留下你的画布项目规模------我想收集真实场景下的 n 值。

相关推荐
JavaGuide42 分钟前
GPT-6 Sol 和 Claude Opus 5.5 发布,直接杀死比赛!
前端·后端
计算机魔术师42 分钟前
Claude Opus 5.5 登顶 Artificial Analysis 智能指数,得分 58
前端
夏天要喝冰可乐42 分钟前
WorkBuddy 每天自动领积分!教你用云函数做个签到机器人
前端·python
Amos_Web43 分钟前
Rspack 源码解析(十四):Resolver 与 NormalModuleFactory
前端·rust·源码
PC2005_cloud43 分钟前
Spring Boot 事务回滚方法
前端·后端
颜进强43 分钟前
装了个 AI Skill 却查不了数据?一篇讲透 Skill 调用接口的三种方式(scripts / CLI / MCP)
前端·后端·ai编程
无责任此方_修行中44 分钟前
插件+1:MiaoMint —— 类 RayCast 的标签管理工具
前端·javascript·vibecoding
IT_陈寒44 分钟前
Redis误删数据后的血泪教训:我竟然这样找回来了
前端·人工智能·后端
去伪存真1 小时前
从零到一开发一个英语情景教学Agent
前端·人工智能