Skip to content

Commit 218f5a1

Browse files
committed
perf(ui): 细粒度 selector 订阅与键入防抖,消除整页连带重渲染
- AppLayout/Step5ExitDemo 改为逐字段 selector 订阅 OrbitalStore,避免 hoveredModule 等高频字段变化连带重渲染整棵布局树 - NoteEditPage 弃用 useShallow(s => s) 整 store 订阅;健康度文本改为 1s 防抖 + 面板打开时惰性取快照,键入路径不再每键触发 457 行整页重渲染
1 parent c3be476 commit 218f5a1

3 files changed

Lines changed: 47 additions & 8 deletions

File tree

‎client/src/components/layout/AppLayout.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,14 @@ import { PERFORMANCE_MODE_CONFIG } from '@/lib/performance/performanceMode';
3131
export default function AppLayout() {
3232
const { pathname } = useLocation();
3333
const navigate = useNavigate();
34-
const { isInModule, currentModule, overlayVisible, enterModule, exitModule, syncWithRoute } = useOrbitalStore();
34+
// 细粒度 selector 订阅:整 store 订阅会被 hoveredModule 等高频字段变化
35+
// 连带重渲染整棵布局树(含 Outlet 页面),是流畅度关键瓶颈
36+
const isInModule = useOrbitalStore((s) => s.isInModule);
37+
const currentModule = useOrbitalStore((s) => s.currentModule);
38+
const overlayVisible = useOrbitalStore((s) => s.overlayVisible);
39+
const enterModule = useOrbitalStore((s) => s.enterModule);
40+
const exitModule = useOrbitalStore((s) => s.exitModule);
41+
const syncWithRoute = useOrbitalStore((s) => s.syncWithRoute);
3542
const openHelp = useOnboardingStore((s) => s.openHelp);
3643
const [showCloseDialog, setShowCloseDialog] = useState(false);
3744
const { sync } = useSync();

‎client/src/components/onboarding/steps/Step5ExitDemo.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import { useOrbitalStore } from '@/lib/3d/navigation/OrbitalStore';
1313

1414
export function Step5ExitDemo() {
1515
const { nextStep, autoDemo } = useOnboardingStore();
16-
const { isInModule, exitModule } = useOrbitalStore();
16+
// 细粒度 selector:避免 hoveredModule 等高频字段连带重渲染
17+
const isInModule = useOrbitalStore((s) => s.isInModule);
18+
const exitModule = useOrbitalStore((s) => s.exitModule);
1719
const navigate = useNavigate();
1820
const text = STEP_TEXTS[4];
1921

‎client/src/features/notes/pages/NoteEditPage.tsx‎

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import { useEffect, useCallback, useRef, useMemo, useState } from 'react';
1010
import { useNavigate, useParams } from 'react-router-dom';
1111
import { EditorContent } from '@tiptap/react';
12-
import { useShallow } from 'zustand/react/shallow';
1312
import { useNoteStore } from '../store/useNoteStore';
1413
import { CornellLayout } from '../components/CornellLayout';
1514
import FreeCanvas from '../components/FreeCanvas';
@@ -60,7 +59,12 @@ export default function NoteEditPage() {
6059
},
6160
});
6261

63-
const { notes, updateNote, selectNote, loadNotes } = useNoteStore(useShallow(s => s));
62+
// 细粒度 selector:整 store 订阅(useShallow(s => s))会在任何字段变化时
63+
// 重渲染整页,键入场景下与 healthText 叠加放大卡顿
64+
const notes = useNoteStore((s) => s.notes);
65+
const updateNote = useNoteStore((s) => s.updateNote);
66+
const selectNote = useNoteStore((s) => s.selectNote);
67+
const loadNotes = useNoteStore((s) => s.loadNotes);
6468
const note = notes.find((n) => n.id === noteId) || null;
6569

6670
const titleRef = useRef<HTMLInputElement>(null);
@@ -82,13 +86,27 @@ export default function NoteEditPage() {
8286
const [tierOpen, setTierOpen] = useState(false);
8387

8488
// === N3 笔记健康度:跟踪编辑器实时文本供工具栏指示器计算 ===
89+
// 流畅度修复:原版每次键入同步 setHealthText 导致 457 行整页每键重渲染。
90+
// 现改为 1s 防抖 + 打开消费面板时惰性取最新快照,键入路径不再触发整页重渲染
8591
const [healthText, setHealthText] = useState('');
8692
useEffect(() => {
8793
if (!editor) return;
8894
setHealthText(editor.getText());
89-
const onHealthUpdate = () => setHealthText(editor.getText());
95+
let timer: ReturnType<typeof setTimeout> | null = null;
96+
const onHealthUpdate = () => {
97+
if (timer) clearTimeout(timer);
98+
timer = setTimeout(() => setHealthText(editor.getText()), 1000);
99+
};
90100
editor.on('update', onHealthUpdate);
91-
return () => { editor.off('update', onHealthUpdate); };
101+
return () => {
102+
editor.off('update', onHealthUpdate);
103+
if (timer) clearTimeout(timer);
104+
};
105+
}, [editor]);
106+
107+
/** 惰性快照:打开消费 healthText 的面板前取编辑器最新文本,避免防抖窗口内数据滞后 */
108+
const refreshHealthText = useCallback(() => {
109+
if (editor) setHealthText(editor.getText());
92110
}, [editor]);
93111

94112
// === N6 概念冲突检测:内容稳定后自动比对新旧理解 ===
@@ -244,6 +262,18 @@ export default function NoteEditPage() {
244262
soundPlayer.play('note_manual_save');
245263
};
246264

265+
// 合书测试入口:先同步最新文本快照再打开(healthText 为防抖值,可能滞后≤ 1s)
266+
const handleOpenClosedBook = useCallback(() => {
267+
refreshHealthText();
268+
setClosedBook(true);
269+
}, [refreshHealthText]);
270+
271+
// 内容分层入口:同理先取最新快照
272+
const handleOpenTier = useCallback(() => {
273+
refreshHealthText();
274+
setTierOpen(true);
275+
}, [refreshHealthText]);
276+
247277
// 阶段四:导出当前笔记为 Markdown(导图笔记降级为大纲)
248278
const handleExportMarkdown = () => {
249279
if (!note) return;
@@ -364,7 +394,7 @@ export default function NoteEditPage() {
364394
<div className="absolute right-4 top-4 z-10 flex flex-col gap-2">
365395
<Tip text="合书测试:隐藏笔记,凭回忆自测" side="left">
366396
<button
367-
onClick={() => setClosedBook(true)}
397+
onClick={handleOpenClosedBook}
368398
className="p-2 rounded-full bg-bg-primary/80 backdrop-blur-sm border border-border/40 text-text-tertiary hover:text-brand-600 hover:border-brand-300 shadow-sm transition-all duration-200"
369399
>
370400
<EyeOff className="w-4 h-4" strokeWidth={1.5} />
@@ -373,7 +403,7 @@ export default function NoteEditPage() {
373403
{/* N5 内容分层入口:策略性遗忘标记 */}
374404
<Tip text="内容分层:聚焦核心概念" side="left">
375405
<button
376-
onClick={() => setTierOpen(true)}
406+
onClick={handleOpenTier}
377407
className="p-2 rounded-full bg-bg-primary/80 backdrop-blur-sm border border-border/40 text-text-tertiary hover:text-brand-600 hover:border-brand-300 shadow-sm transition-all duration-200"
378408
>
379409
<Layers className="w-4 h-4" strokeWidth={1.5} />

0 commit comments

Comments
 (0)