feat(editor): 大文档异步分片载入(docs/20)

>6 万字符文档按顶层结构安全切分:第一片立即建编辑器,其余片逐片解析
追加、片间让出主线程,窗格左下角显示载入进度;载入期间可切标签、编辑
其他文档。追加期静音回写、状态保持磁盘完整原文,载完一次性对齐。
分片与整篇解析的序列化等价有测试锁定(tests/chunk-load.test.ts)。
此前试做的已关闭重开缓存已按用户要求整笔撤回。
This commit is contained in:
Muse
2026-11-14 15:16:23 +08:00
parent 2d1cd3045a
commit 2d0843dd97
5 changed files with 325 additions and 1 deletions

48
docs/20-async-loading.md Normal file
View File

@@ -0,0 +1,48 @@
# 大文档异步载入(2026-11-14,取代已撤回的「重开缓存」方案)
## 需求(用户口径)
大文件打开慢可以接受,但不能阻塞其他操作:打开要在后台进行,给出加载中
状态,用户能切标签、编辑别的文档、点菜单。
## 为什么不是真 Web Worker 线程
实测探针:Milkdown 的解析管线离不开 DOM——headless Editor 在 Node 里
create() 直接抛 `document is not defined`(core 的 editorView 插件在装配期
就引用 document.body,其后 ProseMirror 视图构建也需要真实 DOM)。
把整套 DOM 模拟进 Worker 不可靠,故采用主线程分片让行达到同样的
「不阻塞」效果:每片工作控制在几十毫秒量级,片间让出主线程。
## 方案(src/editor/chunkLoader.ts + EditorPane)
- 阈值:内容 > 60,000 字符且可切分时走分片;否则维持同步打开。
- 切分(splitMarkdownChunks):围栏感知扫描,只在「空行 + ATX 标题 /
围栏开始 / 分隔线」处切——这些位置之前的块必然已闭合,片段独立解析
与整篇解析等价(等价性有自动化测试锁定);按约 24,000 字符攒片。
文档含链接引用定义或脚注定义时不切(定义可位于文末被前文引用),
整篇一片同步解析(与此前行为一致)。
- 载入:第一片直接建编辑器(马上能看、能滚);其余片借一台隐藏的同厂
编辑器取 parserCtx 逐片解析,经 JSON 转交给目标编辑器(两台编辑器的
schema 是不同实例,直接插节点会被 ProseMirror 静默丢弃——实测踩过,
已加「追加未生效即抛错」的断言),追加到文末(不进撤销历史),每片后
setTimeout(0) 让行并更新进度。
- 进度:窗格左下角徽标「正在载入文档… N%(可切换到其他文档继续工作)」,
不遮挡已载入内容;切到别的标签时它在隐藏窗格里继续载完。
- 状态一致性:追加期间静音序列化回写,标签状态里始终是磁盘上的完整
原文(自动保存/恢复/外部变更检测都不受影响);载完一次性以最终序列化
对齐(不标脏)。载入中编辑已载入部分是允许的,最终对齐会把编辑一并
收进去。若某片解析/追加失败:解除静音、把已载入部分如实回写为
未保存状态,绝不冒充完整文档。
## 验证
- tests/chunk-load.test.ts 5 例:切分拼接逐字还原、围栏内假标题不切、
引用定义整篇一片、小文档不分片;66K 字符富结构文档(标题/宽松列表/
任务/表格/围栏/Mermaid/公式/引用/分隔线)分片载入的序列化与整篇解析
**逐字相等**。全量 vitest 94 通过 + 2 跳过;tsc 0;构建通过。
- 无头浏览器实测(596K 字符文档):徽标从 13% 起显示;载入中真实鼠标
点击切到其他标签立即生效(徽标在其隐藏窗格里继续涨到 22%…),在其
编辑器里打字实时落字;全程无页面错误;载完 180 个二级标题齐全。
总耗时约 10.6 秒为本机无头慢速口径,WebView2 真机应明显更快;
关键不在总时长,而在全程界面可操作。
- 待真机验:桌面模式打开 100K+ 文档时的进度徽标与切换手感。