History

perf: debounce Zustand store persistence and deduplicate legacy cleanup

The useStore subscriber called persistState on every state mutation, triggering JSON.stringify + localStorage.setItem synchronously. It also ran 8 localStorage.removeItem calls for legacy keys on every fire. Wrap the subscriber with a 500ms debounce so rapid state changes batch into a single write. Move legacy key cleanup behind a one-time flag so it runs only once per page load. Add a beforeunload handler to flush the final state.

Chukwudi Nwobodo committed 8 months ago

Commit dd51d4f1 parent e2168731 file · +32 −4Browse files

Unified · Split

apps/web/src/store.ts+32−4
@@ -62,6 +62,8 @@ function readPersistedState(): AppState { · 61 unmodified lines
62 }62 }
63}63}
64 64
65let legacyKeysCleanedUp = false;
66
65function persistState(state: AppState): void {67function persistState(state: AppState): void {
66 if (typeof window === "undefined") return;68 if (typeof window === "undefined") return;
67 try {69 try {
@@ -73,14 +75,29 @@ function persistState(state: AppState): void { · 5 unmodified lines
73 .map((project) => project.cwd),75 .map((project) => project.cwd),
74 }),76 }),
75 );77 );
76 for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) {78 if (!legacyKeysCleanedUp) {
77 window.localStorage.removeItem(legacyKey);79 legacyKeysCleanedUp = true;
80 for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) {
81 window.localStorage.removeItem(legacyKey);
82 }
78 }83 }
79 } catch {84 } catch {
80 // Ignore quota/storage errors to avoid breaking chat UX.85 // Ignore quota/storage errors to avoid breaking chat UX.
81 }86 }
82}87}
83 88
89let persistTimer: ReturnType<typeof setTimeout> | null = null;
90
91function debouncedPersistState(state: AppState): void {
92 if (persistTimer !== null) {
93 clearTimeout(persistTimer);
94 }
95 persistTimer = setTimeout(() => {
96 persistTimer = null;
97 persistState(state);
98 }, 500);
99}
100
84// ── Pure helpers ──────────────────────────────────────────────────────101// ── Pure helpers ──────────────────────────────────────────────────────
85 102
86function updateThread(103function updateThread(
@@ -394,8 +411,19 @@ export const useStore = create<AppStore>((set) => ({ · 307 unmodified lines
394 set((state) => setThreadBranch(state, threadId, branch, worktreePath)),411 set((state) => setThreadBranch(state, threadId, branch, worktreePath)),
395}));412}));
396 413
397// Persist on every state change414// Persist state changes with debouncing to avoid localStorage thrashing
398useStore.subscribe((state) => persistState(state));415useStore.subscribe((state) => debouncedPersistState(state));
416
417// Flush pending writes synchronously before page unload to prevent data loss.
418if (typeof window !== "undefined") {
419 window.addEventListener("beforeunload", () => {
420 if (persistTimer !== null) {
421 clearTimeout(persistTimer);
422 persistTimer = null;
423 persistState(useStore.getState());
424 }
425 });
426}
399 427
400export function StoreProvider({ children }: { children: ReactNode }) {428export function StoreProvider({ children }: { children: ReactNode }) {
401 useEffect(() => {429 useEffect(() => {