perf: 大文档性能优化——1.getMarkdown 防抖 300ms: 2000-5000 行文档每次按键序列化全量 markdown 开销大, 改为 debounce 300ms; 2.dsh-flush-body 事件: Ctrl+S/关窗/切文档前同步编辑器内容到 store, 确保保存不丢内容; 3.openDoc 不 await saveNow: 旧文档保存与新文档读取并行, 加快切换速度; 4.自动保存间隔 1s→3s: 大文档每秒写盘 I/O 压力大; 5.修复 save-mechanism E2E 测试时序适配新防抖

This commit is contained in:
hunter-dev
2026-08-27 15:48:19 +08:00
parent 3fe35ef873
commit ebd955ce4b
4 changed files with 32 additions and 8 deletions
+2 -2
View File
@@ -49,7 +49,7 @@ export function App() {
}).catch(() => {}); } }, [activeId]);
// FR-S4:关窗前冲刷未保存内容
useEffect(() => {
const onBeforeUnload = () => { try { useStore.getState().saveNow(); } catch {} };
const onBeforeUnload = () => { try { window.dispatchEvent(new CustomEvent('dsh-flush-body')); useStore.getState().saveNow(); } catch {} };
window.addEventListener('beforeunload', onBeforeUnload);
return () => window.removeEventListener('beforeunload', onBeforeUnload);
}, []);
@@ -62,7 +62,7 @@ export function App() {
await useStore.getState().loadTree();
await useStore.getState().openDoc(path);
},
onSave: () => useStore.getState().saveNow(),
onSave: () => { window.dispatchEvent(new CustomEvent('dsh-flush-body')); useStore.getState().saveNow(); },
onFocusSearch: () => { const el = document.querySelector('input[placeholder*="搜索"]') as HTMLInputElement; el?.focus(); },
onToggleOutline: () => layout.toggleOutline(),
onCheat: () => setShowCheat(true),
@@ -44,6 +44,7 @@ export function TiptapEditor() {
const setBody = useStore(s => s.setBody);
const skipUpdate = useRef(false);
const editorRef = useRef<Editor | null>(null);
const mdTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
// editor 实例稳定,不随文档切换重建(避免销毁/重建过渡期 commands 为 null)
const editor = useEditor({
@@ -92,7 +93,14 @@ export function TiptapEditor() {
return false;
},
},
onUpdate: ({ editor }) => { if (!skipUpdate.current) setBody((editor.storage as Record<string, any>).markdown.getMarkdown()); },
onUpdate: ({ editor }) => {
if (skipUpdate.current) return;
// 大文档防抖——getMarkdown() 对 2000+ 行文档开销显著,不必每次按键都序列化
if (mdTimer.current) clearTimeout(mdTimer.current);
mdTimer.current = setTimeout(() => {
setBody((editor.storage as Record<string, any>).markdown.getMarkdown());
}, 300);
},
});
// 切换文档时只同步内容,不重建 editor
@@ -111,6 +119,17 @@ export function TiptapEditor() {
skipUpdate.current = false;
}, [currentPath, editor]);
// 监听保存前冲刷——Ctrl+S / 关窗前同步编辑器内容到 store
useEffect(() => {
if (!editor) return;
const onFlush = () => {
if (mdTimer.current) { clearTimeout(mdTimer.current); mdTimer.current = null; }
setBody((editor.storage as Record<string, any>).markdown.getMarkdown());
};
window.addEventListener('dsh-flush-body', onFlush);
return () => window.removeEventListener('dsh-flush-body', onFlush);
}, [editor, setBody]);
if (!currentPath) {
return (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.4 }}>
+4 -3
View File
@@ -66,8 +66,9 @@ export const useStore = create<EditorState>((set, get) => ({
},
openDoc: async (path: string) => {
// 切换前冲刷当前文档
await get().saveNow();
// 切换前冲刷当前文档——先同步编辑器内容到 store 再保存
window.dispatchEvent(new CustomEvent('dsh-flush-body'));
void get().saveNow();
const doc: ReadDocResult = await invoke(IPC.docRead, { path });
set({ currentPath: path, body: doc.body, meta: doc.meta, title: doc.meta.title ?? path.split('/').pop()!.replace(/\.md$/, ''), saveState: 'saved', docMtime: doc.mtime });
// 同步到标签页(如已存在则激活,否则新建)
@@ -103,7 +104,7 @@ export const useStore = create<EditorState>((set, get) => ({
set({ body, saveState: 'saving' });
// FR-S1:防抖自动保存——间隔读 settings.autoSaveInterval(秒→毫秒)
if (saveTimer) clearTimeout(saveTimer);
const interval = (() => { try { const s = JSON.parse(localStorage.getItem('dsh-settings') ?? '{}'); return (s.autoSaveInterval ?? 1) * 1000; } catch { return 1000; } })();
const interval = (() => { try { const s = JSON.parse(localStorage.getItem('dsh-settings') ?? '{}'); return (s.autoSaveInterval ?? 3) * 1000; } catch { return 3000; } })();
saveTimer = setTimeout(() => { get().saveNow(); }, interval);
},
@@ -62,7 +62,8 @@ test('FR-S4:关窗冲刷未保存内容', async () => {
await a.page.keyboard.type('before-close-flush');
// 等一下让内容进入 store
await a.page.waitForTimeout(200);
// 手动触发 saveNow(模拟 beforeunload 冲刷)
// 冲刷编辑器内容到 store 再保存(模拟 beforeunload 冲刷)
await a.page.evaluate(() => window.dispatchEvent(new CustomEvent('dsh-flush-body')));
await a.page.evaluate(() => (window as any).__STORE__?.getState?.().saveNow?.());
await a.page.waitForTimeout(500);
await a.app.close();
@@ -86,7 +87,10 @@ test('FR-S6:reloadDoc 检测外部改动', async () => {
await a.page.locator('.ProseMirror').click();
await a.page.keyboard.type('original');
await a.page.waitForTimeout(2000); // 等待自动保存
await a.page.waitForTimeout(200);
await a.page.evaluate(() => window.dispatchEvent(new CustomEvent('dsh-flush-body')));
await a.page.evaluate(() => (window as any).__STORE__?.getState?.().saveNow?.());
await a.page.waitForTimeout(500); // 等待保存完成
// 外部修改文件
const filePath = nodePath.join(vault, '重载测试.md');