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 不再显示可见
PreCodeNodefallback。 - 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。
验证
库侧验证:
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 的集成验证:
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 --checke2e 覆盖:
- 已加载 thread 来回切换后,保存的滚动位置不变化。
- 当前 thread 滚动后刷新,恢复到刷新前位置。
- code block warm remount 不出现可见
PreCodeNodefallback。 - Mermaid 和 code block 同时出现在可视区域时,切换和刷新后 row 位置、高度、header 和后续正文稳定。
- Infographic 和 code block 同时出现在可视区域时,切换和刷新后 row 位置、高度、header 和后续正文稳定。