e0a5f57793
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
108 lines
6.8 KiB
Markdown
108 lines
6.8 KiB
Markdown
# Heavy block remount jank
|
||
|
||
| 字段 | 值 |
|
||
|------|-----|
|
||
| 优先级 | P1 |
|
||
| 分类 | renderer / heavy nodes / async components |
|
||
| 状态 | done |
|
||
| 创建日期 | 2026-04-30 |
|
||
| 关联文件 | `src/components/CodeBlockNode/CodeBlockNode.vue`、`src/components/CodeBlockNode/monaco.ts`、`src/components/CodeBlockNode/runtime.ts`、`src/components/MermaidBlockNode/MermaidBlockNode.vue`、`src/components/InfographicBlockNode/InfographicBlockNode.vue`、`src/components/NodeRenderer/NodeRenderer.vue`、`src/utils/diagramHeight.ts`、`src/types/component-props.ts`、`packages/markstream-vue2/src/components/NodeRenderer/NodeRenderer.vue`、`packages/markstream-vue2/src/utils/diagramHeight.ts`、`packages/markstream-react/src/renderers/renderNode.tsx`、`packages/markstream-angular/src/components/shared/node-outlet-helpers.ts` |
|
||
|
||
## 问题
|
||
|
||
使用虚拟滚动并恢复已保存 `scrollTop` 的上层应用,在重新挂载已经加载过的内容时,重型 Markdown block 会出现可见抖动。最明显的是以下几类节点:
|
||
|
||
- `CodeBlockNode` 重新挂载时先显示 `PreCodeNode`,再切换到 Monaco code block。
|
||
- `MermaidBlockNode` 在 async component resolve 前可能先出现空白或源码 fallback,再替换成图表。
|
||
- `InfographicBlockNode` 存在同类问题:async component 没有 loading shell,并且组件先以源码模式初始化,mounted 后才切到 preview。
|
||
|
||
这些行为对冷启动首次渲染是合理的,但对已加载内容的 remount 不稳定。上层虚拟滚动已经恢复了 `scrollTop` 后,block 的 DOM 结构和高度又发生变化,就会把下方正文推开,表现为闪烁、内容瞬移,或者落入虚拟列表 stale spacer 造成的空白区域。
|
||
|
||
这是 `markstream-vue` 层的问题。上层应用可以保存和恢复滚动位置,但如果组件首帧高度、fallback 模式和最终渲染结果不一致,应用层无法稳定消除这类抖动。
|
||
|
||
后续排查确认同类风险不只存在于 Vue 3 包。`markstream-vue2` 的 Mermaid/Infographic 仍然使用异步组件首帧空壳,`markstream-react` 和 `markstream-angular` 虽然没有同样的 async component 空壳,但 Mermaid/Infographic 首帧仍缺少稳定的高度估算,切换或刷新时也可能推动后续正文。
|
||
|
||
## 根因
|
||
|
||
- code block 是否显示 fallback 只依赖当前组件实例是否完成 editor mount,没有感知共享 Monaco runtime 是否已经预热完成。
|
||
- Mermaid 和 Infographic 使用默认 async component loading 行为,首帧没有预留最终图表容器的形态和高度。
|
||
- Mermaid 和 Infographic 的 preview 高度需要等真实 SVG/HTML 渲染后才能测量。
|
||
- Infographic 默认 `showSource = true`,loader 可用时再在 mounted 后切到 preview,导致 remount 时可能先显示源码或空白。
|
||
- loading shell 和最终 header 的 padding、图标、action button 占位不一致时,即使只有几像素差异,也会让图表下方正文产生可见位移。
|
||
|
||
## 修复
|
||
|
||
### CodeBlockNode
|
||
|
||
- 增加共享 code block runtime ready 状态和 `preloadCodeBlockRuntime()`。
|
||
- `getUseMonaco()` 在 Monaco 以及 preload 工作完成后标记 runtime ready。
|
||
- runtime 已 ready 的 warm remount 不再显示可见 `PreCodeNode` fallback。
|
||
- cold start 仍然保留 fallback,直到 editor mount 和初始高度稳定。
|
||
- editor 创建失败时继续显示 fallback,保证代码内容仍可读。
|
||
- 对齐 fallback 的 font size、line height、padding 和估算内容高度,减少 fallback 到 Monaco 过程中的高度变化。
|
||
|
||
### MermaidBlockNode
|
||
|
||
- 增加基于图表源码的 preview 高度估算。
|
||
- `NodeRenderer` 在调用方没有显式传入时自动注入 `estimatedPreviewHeightPx`。
|
||
- async component resolve 前立即渲染同形态 loading shell,包括 header 和预留 preview 区域。
|
||
- Mermaid loader 已启用时,组件直接以 preview 模式初始化,不再先显示源码。
|
||
- 最终 preview 在真实内容可用前使用估算高度作为下限。
|
||
|
||
### InfographicBlockNode
|
||
|
||
- 增加和 Mermaid 同类的 preview 高度估算与 async loading shell。
|
||
- Infographic loader 已启用时,组件直接以 preview 模式初始化。
|
||
- 最终 preview 在真实内容可用前使用估算高度作为下限。
|
||
- loading shell 的 header 尺寸与最终 header 对齐,包括图标和 action button 占位,避免 async component resolve 时推动下方正文。
|
||
|
||
### 多框架对齐
|
||
|
||
- `markstream-vue2` 补齐 Mermaid/Infographic 的高度估算、async loading shell 和 `estimatedPreviewHeightPx` 注入。
|
||
- `markstream-react` 在 client/server render path 中都为 Mermaid/Infographic 注入稳定高度估算,并让组件首帧使用该高度。
|
||
- `markstream-angular` 在 NodeOutlet custom inputs 和内置 Mermaid/Infographic 组件中使用同一套高度估算,避免 custom renderer 和 builtin renderer 表现不一致。
|
||
- React/Angular 的 CodeBlock/MarkdownCodeBlock 没有 Vue3 `PreCodeNode -> CodeBlockNode` 的同一条切换链路,本次只补图表高度稳定性,不改它们的 code block 架构。
|
||
|
||
## 为什么这样改
|
||
|
||
这次修复把责任放在产生不稳定 DOM 的组件内部:
|
||
|
||
- 上层应用负责保存滚动位置、恢复滚动位置、维护虚拟列表 row measurement。
|
||
- `markstream-vue` 负责让重型 block 的首帧 DOM 形态和高度尽量接近最终状态,避免破坏上层滚动恢复。
|
||
|
||
使用 loading shell 和 estimated height floor 比关闭虚拟滚动、或者要求每个应用分别特判 Mermaid/Infographic/CodeBlock 更收敛。optional peer fallback 行为也被保留:Mermaid、Infographic 或 Monaco 不可用时,仍然回退到可读的 preformatted code。
|
||
|
||
## 验证
|
||
|
||
库侧验证:
|
||
|
||
```bash
|
||
pnpm vitest run test/node-renderer-heavy-node-props.test.ts
|
||
pnpm vitest run test/vue2-node-renderer-heavy-node-props.test.ts
|
||
pnpm vitest run test/react-render-node-heavy-props.test.ts test/react-diagram-block-parity.test.tsx
|
||
pnpm vitest run test/markstream-angular-node-outlet.test.ts
|
||
pnpm vitest run test/code-block-editor-lock.test.ts
|
||
pnpm vitest run test/mermaid-streaming-preview-regression.test.ts
|
||
pnpm lint
|
||
pnpm typecheck
|
||
pnpm build
|
||
git diff --check
|
||
```
|
||
|
||
通过 `dimcode-app` workspace link 的集成验证:
|
||
|
||
```bash
|
||
pnpm test:e2e:scroll
|
||
DIMCODE_E2E_REAL_THREAD_IDS=924740a7-8e64-454b-b4bf-7a3b462ce6bf,s_1777337966876_0 pnpm test:e2e:scroll
|
||
pnpm --filter @dimcode/renderer typecheck
|
||
git diff --check
|
||
```
|
||
|
||
e2e 覆盖:
|
||
|
||
- 已加载 thread 来回切换后,保存的滚动位置不变化。
|
||
- 当前 thread 滚动后刷新,恢复到刷新前位置。
|
||
- code block warm remount 不出现可见 `PreCodeNode` fallback。
|
||
- Mermaid 和 code block 同时出现在可视区域时,切换和刷新后 row 位置、高度、header 和后续正文稳定。
|
||
- Infographic 和 code block 同时出现在可视区域时,切换和刷新后 row 位置、高度、header 和后续正文稳定。
|