Files
mnote/bugs/04-tree-domain/done/4-25-tree-command-create-delete-reload-latency-v1.md
T
lix-2026 96e03645f7 chore: 收口 review 执行清单与 runtime 验证
- 补齐 design/10-review 执行清单、验收标准与相关设计治理记录

- 迁移已完成的 tree、mindmap、runtime fallback、AI kernel 等设计和缺陷条目

- 推进 Rust Web runtime、tree/sidebar、page aggregate、mindmap 与 OnlyOffice 路由侧验证支撑

- 增加 task177-task180 smoke/audit 脚本及前端相关测试覆盖
2026-05-14 05:52:08 +08:00

13 KiB
Raw Blame History

4-25 [done][bug] 树命令新建/删除后强制刷新导致卡顿 v1

更新时间:2026-05-14

分类归属:

  • 04-tree-domain/done
  • 关联缺陷:bugs/05-editor-mainline/done/5-11-mindmap-ghost-assets-and-tree-command-latency-v1.md

用户反馈:

  • “删除页面和新建页面都很慢,不知道为什么这么卡。”

1. 问题定义

当前页面树/文件树的新建页面、删除页面等命令,在服务端命令成功后仍会触发整页刷新或完整树刷新。用户感知是:新建和删除并不是局部更新,而是卡一下、等整套页面壳或树壳重新加载。

这属于 tree command -> projection update -> UI apply 链路问题,应归到 04-tree-domain,而不是只作为编辑器 UI 问题处理。

2. 真实现象

用户反馈:

  1. 新建页面慢。
  2. 删除页面慢。
  3. 卡顿与同轮 mindmap 文件树重复增长一起出现,怀疑树刷新链路被频繁触发。

期望结果:

  1. tree.node.create 成功后,树 UI 局部插入新节点并进入重命名/导航状态。
  2. tree.node.archive 成功后,树 UI 局部移除节点或移动到回收站 projection。
  3. 只有 delta 无法应用、projection 缺失、SSE 断线或数据不一致时,才进入 resync/reload fallback。
  4. 普通新建/删除不应默认整页 reload。

3. 证据

3.1 Rust tree shell 有硬刷新路径

rust/crates/mnote-web/src/routes/tree.rs:2976:2986

  • scheduleRefresh() 在 80ms 后执行 window.location.reload()
  • 如果带 renameRowId,则通过 window.location.assign(url.toString()) 重新加载。

调用点:

  • 文件树删除:rust/crates/mnote-web/src/routes/tree.rs:2791:2824
  • 新建页面:rust/crates/mnote-web/src/routes/tree.rs:4309:4340
  • 重命名:rust/crates/mnote-web/src/routes/tree.rs:4373:4406
  • 移动:rust/crates/mnote-web/src/routes/tree.rs:4448:4502

这条路径会让 tree shell 命令体验明显慢于纯 delta / reducer 更新。

3.2 主页面已有 tree live controller,但命令后仍刷新

主页面已经监听 tree live

  • rust/crates/mnote-web/src/ssr/pages/layout.rs:3881:3917 处理 tree:snapshot / tree:delta / tree:resync
  • rust/crates/mnote-web/src/ssr/pages/layout.rs:3939:4050 启动 /api/tree/events EventSource

也就是说,命令完成后理论上可以由 delta/resync 更新 projection;当前硬 reload 与 live projection 机制重复。

3.3 命令本身已有 delta hint

Bridge runtime 对树命令生成 stream delta hint

  • tree.node.createrust/crates/bridge-runtime/src/lib.rs:9545:9592
  • tree.node.archiverust/crates/bridge-runtime/src/lib.rs:9655:9702

当前前端没有把这些结果作为默认局部更新来源,而是在 tree shell 中继续 reload。

3.4 React Sidebar 路径也存在重刷链

子代理只读调查发现 React Sidebar 路径也存在重刷:

  • 新建页面:wolai-frontend/src/components/sidebar/sidebar.tsx:1799:1847 附近在 command 后 await refreshTree() 再跳转。
  • 删除页面:wolai-frontend/src/components/sidebar/sidebar.tsx:2148 附近删除后等待 command、refreshTree()、广播文档变化/跳转。
  • Next API 的 api/tree/commands create/delete 还会执行 workspace/default scaffold、bridge mutation、artifact 记录等多段流程。

这说明卡顿不只来自单个 reload,而是命令后刷新链路偏重。

3.5 tree stream overview 查询曾经全量扫描 workspace 日志

/api/tree/events 的 live 轮询依赖 bridgeLogs:listWorkspaceOverview 读取最新 command/domain event 窗口。2026-05-13 复核发现,该查询原先对 command_logsdomain_events 都是按 workspace 全量 .collect() 后再在内存中过滤、排序和分页;日志量增长后,这会让每轮 SSE 轮询成本随历史日志线性增长。

本轮已完成代码侧收口:

  • command_logs 增加 workspace_id + created_at + id 及 status/page/block 常用过滤索引。
  • domain_events 增加 workspace_id + created_at + id 及 status/aggregate 常用过滤索引。
  • listWorkspaceOverview 改为按索引读取有界窗口,不再全量 collect 后分页。

这只能降低 tree live polling 的背景成本,并不等于新建/删除的强刷新问题已经修完。React Sidebar 的 refreshTree() 和 Rust tree shell 的 scheduleRefresh() 仍需单独收口。

3.6 2026-05-14 tree live 局部 apply 进展

本轮已完成主页面 Rust 3000 下的部分局部 apply 收口:

  • rust/crates/mnote-web/src/transport/convex.rstree create / rename / archive / restore / move 及 compat documents mutation 发往 legacy Convex mutation 前剥离 artifact-only 字段,修复 documents:softDeletestreamDeltaHint / domainEventPlan 等额外字段触发 validator 502 的问题。
  • rust/crates/mnote-web/src/ssr/pages/layout.rstree:deltamove_document 会局部更新页面树与 File Tree 行的 data-parent-idremove_document 会局部移除页面树与 File Tree 行。
  • 已通过:cargo test -p mnote-web convex_command_args_strips -- --nocapture
  • 已通过:cargo test -p mnote-web sidebar_tree_runtime -- --nocapture
  • 已通过:node scripts/task177-tree-move-archive-live-smoke.js,覆盖 move 后页面树 / File Tree 行父节点更新、archive 后两棵树局部移除,以及文档页头保持稳定。

这说明 move / archive 的 Rust 3000 主页面 live reducer 已有可验证进展;当时普通新建/删除、React Sidebar refreshTree() 与耗时阈值仍待后续章节继续收口。后续 3.7-3.9 已补齐这些流转条件。

3.7 2026-05-14 Rust tree shell create / delete no-reload 验收

本轮已完成 Rust /tree debug shell 的普通新建 / 删除强刷新收口:

  • rust/crates/mnote-web/src/routes/tree.rs:新增 addTreeItemLocally() / applyCreatedDocumentLocally()convex_workspace create 成功后直接插入本地树项并进入 inline rename;只有本地 apply 失败时才 fallback 到 scheduleRefresh()
  • rust/crates/mnote-web/src/routes/tree.rs:新增 removeTreeItemEverywhere() / applyRemovedDocumentLocally()convex_workspace delete 成功后从本地 projection 移除对应页面及子项;只有删除后仍能在本地 row map 中看到目标行时才 fallback scheduleRefresh()
  • scripts/task179-tree-create-delete-no-reload-smoke.js:真实浏览器覆盖 /tree?mode=filetree 的 create + delete,以及 /tree?mode=page 的 create;断言操作后 URL 不变、对应 mode 内无新增顶层 navigation。
  • 已通过:cargo test -p mnote-web tree_shell_returns_interactive_html_document -- --nocapture
  • 已通过:cargo test -p mnote-web tree_shell -- --nocapture
  • 已通过:node scripts/task179-tree-create-delete-no-reload-smoke.js,最新结果 ok=truefiletree.createMs=120filetree.deleteMs=178page.createMs=120、两个 mode 的 navigationEvents.length=0

3.8 2026-05-14 React Sidebar 直接 create / delete 收口进展

React Sidebar 的直接新建 / 删除路径已完成一层去阻塞:

  • wolai-frontend/src/components/sidebar/sidebar.tsxhandleCreate() 继续使用 createDocumentCommand() 结果本地 setTree() 插入节点并导航,但不再同步 await refreshTree()
  • wolai-frontend/src/components/sidebar/sidebar.tsx:新增 removeDocumentsFromTree()handleDelete()handleDeleteResourceSelection() 在 delete command 成功后本地移除页面节点,不再同步等待整树 refetch;附件删除仍保留 asset 专用刷新链。
  • wolai-frontend/src/components/sidebar/sidebar-delete-preflight-source.test.ts:新增源级回归断言,防止页面 create/delete 成功路径重新出现同步 await refreshTree()
  • 已按 TDD 验证:新增测试先失败,修改后通过。
  • 已通过:pnpm test -- src/components/sidebar/sidebar-delete-preflight-source.test.tsVitest 实际执行 112 个测试文件、459 个测试)。
  • 已通过:pnpm exec eslint src/components/sidebar/sidebar.tsx src/components/sidebar/sidebar-delete-preflight-source.test.ts,无 errorsidebar.tsx 保留既有 warning。

3.9 2026-05-14 Rust-family React shell mutation 本地 apply 收口

Rust-family React shell 的 host mutation 回调已从“命令成功后默认整树刷新”改为可局部 apply:

  • wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsxtree.node.created / tree.node.renamed / tree.subtree.moved 成功后把 command result 中的 documentIdparentIdtitlesortOrderworkspaceIdexecution 传回 Sidebar。
  • wolai-frontend/src/components/sidebar/tree-shell-iframe-host.tsxlegacy iframe bridge 同步转发 mutation payload 中的局部 apply 字段,保持兼容。
  • wolai-frontend/src/components/sidebar/sidebar.tsxhandleTreeShellMutation 对 create / rename / move 分别本地插入、重命名、移动页面树;只有未知事件或缺少必要字段时保留 refreshTree() 兜底。
  • wolai-frontend/src/components/sidebar/sidebar-delete-preflight-source.test.ts:新增 RED -> GREEN 源级回归断言,防止 onTreeMutation -> refreshTree() 重新退化成默认整树 refetch。
  • 已通过:pnpm test -- src/components/sidebar/sidebar-delete-preflight-source.test.tsVitest 实际执行 112 个测试文件、460 个测试)。
  • 已通过:pnpm exec eslint src/components/sidebar/sidebar.tsx src/components/sidebar/sidebar-delete-preflight-source.test.ts src/components/sidebar/tree-shell-host.tsx src/components/sidebar/tree-shell-surface.tsx src/components/sidebar/tree-shell-dom-host.tsx src/components/sidebar/tree-shell-iframe-host.tsx,无 error;保留既有 warning。

4. 当前判断

当前慢的核心不是“Convex 一定慢”,而是树命令执行后缺少轻量本地 apply:

  1. 命令 result / delta hint 已经具备局部更新信息。
  2. UI 仍经常走 refreshTree()window.location.reload() 或 full snapshot。
  3. resync_required 类事件会触发完整 workspace snapshot,结构性变化越多,越容易造成卡顿。
  4. tree stream overview 全量日志扫描已在代码侧改为有界窗口查询,并已有 task123 / task120 / task177 证明 Rust 3000 下可输出并消费相关 live 事件。
  5. move / archive 已具备局部 DOM reducer 证据。
  6. Rust /tree debug shell 的 convex_workspace 普通新建 / 删除已经通过无 reload 与耗时 smoke。
  7. React Sidebar 直接 create/delete 成功路径已改成本地更新,不再同步等待 refreshTree()
  8. Rust-family React shell 的 create / rename / move host mutation 回调已改成本地 apply,不再默认触发整树 refetch;未知事件或缺字段才保留刷新兜底。

5. 建议验证

修复前先补一条计时 smoke

  1. 登录真实测试账号。
  2. 新建 TEST-TREE-LATENCY-<timestamp> 页面。
  3. 记录 POST /api/tree/commands 响应耗时。
  4. 记录下一次 tree:delta / tree:resync 到达耗时。
  5. 断言过程中是否发生 window.location.reload() 或顶层 navigation。
  6. 删除该测试页面,重复同样计时。
  7. 输出新建/删除从点击到 DOM 稳定的总耗时。

6. 建议修复方向

  1. tree.node.create 成功后直接用 command result 插入本地节点,并只在后台等待 live delta 校准。
  2. tree.node.archive 成功后直接从当前 projection 移除节点,并只在后台等待 live delta 校准。
  3. scheduleRefresh() 改成显式 fallback,只有 reducer 无法应用时才调用。
  4. React Sidebar 的 refreshTree() 应去重、节流,并避免与 SSE/full snapshot 同时触发。
  5. smoke 增加无 reload 断言和耗时阈值。
  6. tree stream 查询模型已完成第一步优化;后续若 polling-backed SSE 继续作为长期实现,还需要补连接数、日志量、延迟边界测试。

7. 流转条件

当前状态:done

Rust tree shell 的 create / delete 强刷新子项已关闭;React Sidebar 直接 create/delete 同步刷新等待已关闭;Rust-family React shell 的 create / rename / move host mutation 回调已改为本地 apply,不再默认整树 refetch。

只有满足以下条件后才能移动到 bugs/04-tree-domain/done/

  1. 新建页面普通路径不再默认整页 reload。Rust /tree convex_workspace 的 File Tree 与 Page Tree create 已由 task179 验证。
  2. 删除页面普通路径不再默认整页 reload。Rust /tree convex_workspace 的 File Tree delete 已由 task179 验证。
  3. command result / tree delta 能局部更新页面树与文件树 projection。create 走 applyCreatedDocumentLocally()delete 走 applyRemovedDocumentLocally()move/archive 由 task177 验证主页面与 File Tree DOM reducer。
  4. fallback reload 只在明确异常条件下触发,并有可观测标记。当前 create/delete 仅在本地 apply 失败或目标行未移除时 fallback scheduleRefresh()
  5. smoke 记录新建/删除耗时,并通过无 reload 断言。task179 最新记录 filetree.createMs=120filetree.deleteMs=178page.createMs=120,对应 mode 内 navigation 为 0。
  6. Rust-family React shell 的 onTreeMutation -> refreshTree() 不再在 create / rename / move 成功后默认整树 refetch。handleTreeShellMutation 已按 create / rename / move 本地 apply,未知事件或缺字段才刷新兜底,并由 sidebar-delete-preflight-source.test.ts 防回归。