Plate 与 Slate 性能基准剖析:宽兄弟数组上的 `set_node` 变换为何昂贵

发布时间:2026/9/15 13:17:35

Plate 与 Slate 性能基准剖析:宽兄弟数组上的 `set_node` 变换为何昂贵 Plate 与 Slate 性能基准剖析宽兄弟数组上的set_node变换为何昂贵【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate导读本文基于仓库内一篇面向 Slate 维护者的基准测试说明docs/plans/2026-03-30-slate-set-node-note-for-maintainer.md完整还原一次从 Plate 性能悬崖追查到 Slate 上游优化缝隙的调查过程。你将看到在超长文档上反复执行精确路径Transforms.setNodes(...)时开销到底出在哪一层——既不是归一化normalization也不是路径/范围引用簿记而是modifyDescendant中不可变祖先数组的反复重写同时结合当前仓库源码印证set_node变换的实现细节并介绍仓库中已经出现的批量精确路径更新方案setNodesBatch如何回应文档提出的优化方向。调查起点Plate 侧的性能悬崖与一次性修复在将 Plate 与 Slate 放在超长文档上进行基准对比时作者遇到了一个巨大的性能悬崖。定位后发现 Plate 自身存在一个 bug初始nodeId归一化normalization阶段对每一个缺失 id 的节点都调用了一次实时的 Slate 变换live transform导致 O(n) 次单点更新串行累积。该问题已在 Plate 侧修复修复方式是直接遍历初始值walk the initial value directly不再对每个缺失 id 走一遍实时变换。这一点值得注意它说明在初始化等一次性场景中应优先用一次性遍历解决问题而不是复用交互式的变换 API。修复 Plate 自身问题之后作者进一步把同一批精确路径更新repeated exact-path update放进slate自身去基准测试得到的结论是稳定的在扁平的超长文档上反复执行Transforms.setNodes(...)耗时主要由set_node变换路径主导主导成本不是归一化主导成本不是 path refs / range refs / dirty-path 簿记主导成本看起来是modifyDescendant中不可变的祖先数组重写immutable ancestor-array rewriting尤其是当父级数组非常宽wide时。需要强调这是一次纯变换基准pure transform benchmark不是 DOM 渲染基准测试结论只针对 Slate 数据模型层的变换开销。复现方式一组可重复的基准命令文档给出了基准文件的原始位置与运行命令。基准文件为packages/slate/test/perf/set-nodes-bench.js文档撰写时的临时产物当前仓库中已不存在复现命令如下bun ./packages/slate/test/perf/set-nodes-bench.js --blocks5000 --group-size50 --repeats5 bun ./packages/slate/test/perf/set-nodes-bench.js --blocks10000 --group-size50 --repeats3参数含义--blocks段落总数5000 或 10000--group-size分组文档中每个 section 父节点下的子节点数量50--repeats重复次数5 或 3段落越多重复次数越少以控制总耗时。基准对比了什么在段落总数相同的前提下基准对比两种文档形态扁平文档flat每个段落都是顶层兄弟节点top-level sibling根节点下是一个宽度等于段落总数的超大children数组分组文档grouped同样的段落被分布到多个 section 父节点下每个 section 恰好容纳50个子节点根节点下是section 数组宽度大幅收窄。对每种形态分别测量 5 个层级Transforms.setNodes(editor, { id }, { at: path })——对每个精确路径逐条调用同样的循环放进Editor.withoutNormalizing(...)中执行在Editor.withoutNormalizing(...)内直接editor.apply({ type: set_node, ... })裸调用modifyDescendant(...)一次遍历重写one-pass rewrite作为下界参照。计时用的apply包装还会进一步拆分ref transforms引用变换、dirty-path 更新、Transforms.transform(...)、Editor.normalize(...)各自的耗时。性能数据宽数组是成本放大器5,000 段落CaseFlatGroupedsetNodesper path73.35 ms22.09 mssetNodesinside outerwithoutNormalizing52.96 msn/adirectapply(set_node)44.62 ms9.11 msbaremodifyDescendant37.01 ms2.38 msone-pass rewrite0.15 ms0.19 ms10,000 段落CaseFlatGroupedsetNodesper path241.36 ms66.19 mssetNodesinside outerwithoutNormalizing169.07 msn/adirectapply(set_node)147.71 ms22.16 msbaremodifyDescendant140.11 ms7.25 msone-pass rewrite0.35 ms0.61 ms数据中有几个非常直观的信号形态敏感性极强同样的段落数从扁平变为分组后各层级的耗时普遍下降 310 倍。10,000段落时裸modifyDescendant从140.11 ms降到7.25 ms约 19 倍差距。setNodes和apply只是叠加开销10,000扁平下setNodesper path241.36 ms→ 直接apply(set_node)147.71 ms→ 裸modifyDescendant140.11 ms逐层剥掉包装后真正的底账几乎全在modifyDescendant。下界极低一次遍历重写one-pass rewrite只有0.15 ms/0.35 ms量级说明数据量本身不是问题问题是每次更新都重写一遍祖先链。计时apply分解10,000扁平段落applyTotalMs146.12 mstransformMs138.00 msdirtyPathsMs2.37 msnormalizeMs1.76 mspath/point/range refs 合计约2.56 ms10,000分组段落applyTotalMs21.81 mstransformMs12.51 msdirtyPathsMs3.78 msnormalizeMs0.78 ms分解结果显示normalizeMs在两种形态下都只有个位数毫秒归一化不是这个工作负载的主导成本真正的重头是transformMs——即变换本身对树结构的不可变重写。refs 与 dirty-path 簿记同样只占毫秒级。结果解读形状敏感性的含义最强的信号是形状敏感性shape sensitivity当宽大的顶层兄弟数组被替换成多个较小的分组数组后同样的段落数、同样的更新次数成本骤降。这强烈说明主要开销并非通用的归一化开销而是反复对祖先链做不可变重写尤其是对宽children数组的重写。modifyDescendant(...)的数字是最清晰的证据5,000扁平37.01 ms5,000分组2.38 ms10,000扁平140.11 ms10,000分组7.25 ms这个层级已经占据了apply(set_node)成本的绝大部分。因此当前行为可以概括为setNodes(...)本身有一定开销apply(...)也有一定开销但真实成本的绝大部分仍然落在set_node变换路径底层的不可变树重写immutable tree rewrite上。源码印证当前仓库中set_node的实现结构文档撰写时指向的源码路径为packages/slate/src/transforms-node/set-nodes.ts——遍历匹配节点并逐个editor.apply({ type: set_node, ... })packages/slate/src/interfaces/transforms/general.ts——set_node分支调用modifyDescendant(...)packages/slate/src/utils/modify.ts——modifyDescendant(...)用replaceChildren(...)重建祖先链而replaceChildren(...)依赖数组切片/展开slicing/spreading。从当前仓库的源码结构看Slate 包已经历内部重构相关实现迁移到了packages/slate/src/internal/transforms/目录setNodes.tssetNodes的入口。值得注意的是它区分两条路径——当传入marks选项时会先解析at、构造匹配match文本节点且父节点非 void 或为 markable void必要时split: true走区间切分路径否则直接调用底层setNodesBase。也就是说区间切分 / match 遍历这些重分支只在需要时启用与文档中无区间切分、无 broad match 遍历时可走快速路径的优化前提一致。setNodes.spec.tsx覆盖marks选项、展开区间先 split 再打标等行为的测试。优化缝隙已被回应仓库中的setNodesBatch文档在可能的上游方向中提出的第一项候选是增加批量精确路径set_node路径batched exact-pathset_nodepath。如果调用方已经持有精确路径共享祖先可以只重建一次而不是每个 op 重建一次。从当前仓库源码看这一方向已经在packages/slate/src/internal/transforms/setNodesBatch.ts中落地为setNodesBatch。其实现要点buildSetNodeBatchOperations(editor, updates)接收{ at: Path; props }[]形式的批量更新逐条做三件事拒绝空路径Cannot set properties on the root node.用NodeApi.get(editor, at)校验节点存在且为 Descendant计算properties旧值与newProperties新值跳过children/text键跳过值未变化value currentValue的键并将 nullish 值从newProperties中剔除与 SlatesetNodes的置空即移除属性语义保持一致。applySetNodeBatchOperations(editor, ops)先校验不存在重复路径setNodesBatch does not support duplicate update paths再在editor.tf.withoutNormalizing(...)内逐个editor.apply(op)。配合 setNodesBatch.spec.tsx 的测试setNodesBatch提供了一次调用、内部批量构造 op、统一跳过无变化更新、归一化仅触发一次的语义。对于文档中描述的需要在大宽兄弟数组上设置大量精确路径属性的工作负载这种批量形态可以直接规避逐条setNodes 逐条归一化的叠加开销是文档所提优化方向的一个具体实现回应。文档提出的上游优化候选方向作为给维护者的备注文档给出了三个候选思路明确标注为候选想法不是成熟提案批量精确路径set_node路径batched exact-pathset_nodepath调用方已持有精确路径时共享祖先只需重建一次而不是每个 op 重建一次。对应仓库现状即上文介绍的setNodesBatch。setNodes内部快速路径internal fast path当满足以下全部条件时启用at是精确Path没有区间切分no range splitting没有 broadmatch遍历更新只是属性替换property replacement。更底层的批量节点属性更新辅助函数apply many node property updates helper在保持 Slate 语义的同时避免对同一父链反复克隆祖先。第 1、2 点本质上都在说同一件事在精确路径 纯属性替换的前提下应当把对共享祖先链的重写次数从 O(更新的节点数 × 祖先深度) 压缩到接近 O(祖先链长度)。边界与注意事项文档对自身结论的适用范围做了明确限定引用时不应越界这是Node 侧微基准microbenchmark不是浏览器挂载mount基准它不能说明 Slate React 渲染的任何问题它不声称normalize在任何情况下都是免费的只说在这个特定的反复精确路径set_node工作负载中归一化不是主导成本触发本次调查的 Plate 侧问题已在 Plate 侧修复初始nodeId归一化改为直接遍历初始值分享这份数据是因为基准结果暗示 Slate 层面存在一个更通用的优化缝隙。结论与可复用的工程经验这次调查可以提炼出几条可迁移的经验性能悬崖要先分清是哪一层的账Plate 侧的问题是初始化时逐 id 走实时变换修复方式直接遍历初始值说明——批量初始化数据时不要复用单点交互变换 API。宽兄弟数组是变换成本的放大器不可变数据结构的祖先链重写成本与父数组宽度直接相关。在无法改变文档形态flat 结构是富文本的常态时应从减少重写次数入手。归一化往往不是主要成本在精确路径、无区间切分的批量更新场景中先做 profile 再优化避免把时间浪费在错误的假设上。优化方向已被实现验证文档提出的批量精确路径更新方向在仓库中已有setNodesBatch实现与配套测试可作为后续维护者在超长文档批量更新场景下的首选 API。如果需要对其他变换形态如insert_node、remove_node在大宽数组上的表现做同样分析可参考本文的测量层级设计从高层 API 逐层剥到裸底层操作并附带 one-pass 下界对照这套方法论本身也值得复用。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/15 13:12:35

CTF音频隐写实战:用Python从WAV噪声中提取Flag

CTF杂项里碰到“WAV音频Python提取Flag”这个组合,几乎每个玩CTF入门的人都会遇到一次。上周帮朋友看一道题,题目只给了一个WAV文件,耳机里听上去从头到尾就是“沙沙”的噪音,语音内容完全没有。很多人卡在这就放弃了,…

2026/9/15 13:12:35

IQ调制与星座图:从正交原理到Python仿真实践

第一次在《通信原理》课本上碰到“IQ调制”四个字,我正在为期末考发愁,满页的三角公式让人一个头两个大。当时满脑子都是“正弦波好好的,干嘛非拆成I路、Q路”“星座图那些点又是什么意思”。后来做了几年无线通信相关工作,从仿真…

2026/9/15 13:32:36

微信小游戏全生命周期实战:从Unity打包到云端运维与降本

这些年做微信小游戏,我最大的感受是:能做出一个跑得起来的 Demo 不难,真正难的是让它一直跑得稳、跑得省、跑得久。从 Unity 里廉几刀出一个包,到微信开发者工具里预览、真机调试,再后端上云、上线、拉新、冲榜、守活动…

2026/9/15 13:32:36

二叉树前序遍历:递归实现与工程实践解析

1. 二叉树前序遍历的递归实现与分析前序遍历是二叉树最基本的操作之一,也是理解递归思想的经典案例。作为数据结构的基础内容,掌握前序遍历不仅能帮助开发者处理树形数据,更能培养递归思维模式。我在处理企业级菜单权限系统时,曾用…

2026/9/15 13:32:36

程序员不可替代的三大硬边界:语义鸿沟、责任闭环与熵减成本

1. 这不是个伪命题,而是每个写代码的人每天都在面对的真实拉锯战“三年了,AI为何还没有抢走程序员饭碗?”——这句话在2024年夏天刷屏技术社区时,我正蹲在客户现场调试一个遗留系统里的定时任务调度器。它用的是十年前的Quartz 2.…

2026/9/15 13:32:36

Typecho宝塔部署实战:环境配置、性能优化与安全加固

1. 为什么Typecho在宝塔面板上部署,比直接手搭更值得投入时间?Typecho不是WordPress,它轻、快、干净,但正因如此,它的部署不像WordPress那样有海量一键安装脚本兜底。很多新手看到“Typecho部署”四个字,第…

2026/9/15 13:32:36

帆软JS开发:控件获取与单元格操作全解析

做了几年帆软报表开发,回头看写得最多的其实不是复杂SQL,也不是花哨的图表配置,而是JavaScript里那几行getWidgetByName和getCellValue。参数联动要取控件值,按钮点击要把结果写进单元格,表单提交前要做校验&#xff0…

2026/9/15 4:54:30

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/15 0:01:16

AI英语单词APP开发:自适应学习算法与移动端优化实践

1. 项目概述 作为一名在移动应用开发领域摸爬滚打多年的老手,我最近完成了一个AI英语单词APP的开发项目。这个项目将传统单词记忆方法与现代AI技术相结合,打造了一款能够智能适应不同用户学习习惯的英语学习工具。 市面上大多数单词APP都存在一个通病&a…

2026/9/15 0:01:16

Flutter与OpenHarmony结合开发手语学习APP实战

1. 项目背景与核心价值作为一名同时接触过Flutter和OpenHarmony的开发者,最近我完成了一个基于Flutter for OpenHarmony的手语学习APP实战项目。这个项目最大的特点在于实现了跨平台框架与国产操作系统深度结合的创新实践——用Flutter开发的应用能完美运行在OpenHa…

2026/9/15 0:01:16

六个月成为机器人工程师:从ROS2到SLAM的实战路径

1. 六个月的紧迫感从哪来:先搞清楚你要成为哪种机器人工程师说实话,六个月的期限并不是一个宽松的时间线。市面上任何一本正经的机器人学教材都超过五百页,ROS2的官方文档可以翻到你怀疑人生,再加上ABB、KUKA这些工业机器人厂家动…

2026/9/14 11:59:31

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/14 13:53:59

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/15 11:42:23

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码