# 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.create`：`rust/crates/bridge-runtime/src/lib.rs:9545` 到 `:9592`
- `tree.node.archive`：`rust/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_logs` 和 `domain_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.rs`：tree create / rename / archive / restore / move 及 compat documents mutation 发往 legacy Convex mutation 前剥离 artifact-only 字段，修复 `documents:softDelete` 因 `streamDeltaHint` / `domainEventPlan` 等额外字段触发 validator 502 的问题。
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`：`tree:delta` 的 `move_document` 会局部更新页面树与 File Tree 行的 `data-parent-id`；`remove_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=true`；`filetree.createMs=120`、`filetree.deleteMs=178`、`page.createMs=120`、两个 mode 的 `navigationEvents.length=0`。

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

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

- `wolai-frontend/src/components/sidebar/sidebar.tsx`：`handleCreate()` 继续使用 `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.ts`（Vitest 实际执行 112 个测试文件、459 个测试）。
- 已通过：`pnpm exec eslint src/components/sidebar/sidebar.tsx src/components/sidebar/sidebar-delete-preflight-source.test.ts`，无 error；`sidebar.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.tsx`：`tree.node.created` / `tree.node.renamed` / `tree.subtree.moved` 成功后把 command result 中的 `documentId`、`parentId`、`title`、`sortOrder`、`workspaceId`、`execution` 传回 Sidebar。
- `wolai-frontend/src/components/sidebar/tree-shell-iframe-host.tsx`：legacy iframe bridge 同步转发 mutation payload 中的局部 apply 字段，保持兼容。
- `wolai-frontend/src/components/sidebar/sidebar.tsx`：`handleTreeShellMutation` 对 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.ts`（Vitest 实际执行 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. [x] 新建页面普通路径不再默认整页 reload。Rust `/tree` `convex_workspace` 的 File Tree 与 Page Tree create 已由 `task179` 验证。
2. [x] 删除页面普通路径不再默认整页 reload。Rust `/tree` `convex_workspace` 的 File Tree delete 已由 `task179` 验证。
3. [x] command result / tree delta 能局部更新页面树与文件树 projection。create 走 `applyCreatedDocumentLocally()`；delete 走 `applyRemovedDocumentLocally()`；move/archive 由 `task177` 验证主页面与 File Tree DOM reducer。
4. [x] fallback reload 只在明确异常条件下触发，并有可观测标记。当前 create/delete 仅在本地 apply 失败或目标行未移除时 fallback `scheduleRefresh()`。
5. [x] smoke 记录新建/删除耗时，并通过无 reload 断言。`task179` 最新记录 `filetree.createMs=120`、`filetree.deleteMs=178`、`page.createMs=120`，对应 mode 内 navigation 为 0。
6. [x] Rust-family React shell 的 `onTreeMutation -> refreshTree()` 不再在 create / rename / move 成功后默认整树 refetch。`handleTreeShellMutation` 已按 create / rename / move 本地 apply，未知事件或缺字段才刷新兜底，并由 `sidebar-delete-preflight-source.test.ts` 防回归。
