From 7a377fc5e4d607a475a7d36957689562d5dcccb9 Mon Sep 17 00:00:00 2001 From: Chukwudi Nwobodo Date: Sun, 8 Mar 2026 06:22:02 +0000 Subject: [PATCH] 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. --- apps/web/src/store.ts | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index 711e993e0fb8a73fe9f4a7b24e365899f5b3acc3..6e816804ccc74f32b124ca4cfaabc7749d19a8b6 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -63,6 +63,8 @@ function readPersistedState(): AppState { } } +let legacyKeysCleanedUp = false; + function persistState(state: AppState): void { if (typeof window === "undefined") return; try { @@ -74,14 +76,29 @@ function persistState(state: AppState): void { .map((project) => project.cwd), }), ); - for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { - window.localStorage.removeItem(legacyKey); + if (!legacyKeysCleanedUp) { + legacyKeysCleanedUp = true; + for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { + window.localStorage.removeItem(legacyKey); + } } } catch { // Ignore quota/storage errors to avoid breaking chat UX. } } +let persistTimer: ReturnType | null = null; + +function debouncedPersistState(state: AppState): void { + if (persistTimer !== null) { + clearTimeout(persistTimer); + } + persistTimer = setTimeout(() => { + persistTimer = null; + persistState(state); + }, 500); +} + // ── Pure helpers ────────────────────────────────────────────────────── function updateThread( @@ -395,8 +412,19 @@ export const useStore = create((set) => ({ set((state) => setThreadBranch(state, threadId, branch, worktreePath)), })); -// Persist on every state change -useStore.subscribe((state) => persistState(state)); +// Persist state changes with debouncing to avoid localStorage thrashing +useStore.subscribe((state) => debouncedPersistState(state)); + +// Flush pending writes synchronously before page unload to prevent data loss. +if (typeof window !== "undefined") { + window.addEventListener("beforeunload", () => { + if (persistTimer !== null) { + clearTimeout(persistTimer); + persistTimer = null; + persistState(useStore.getState()); + } + }); +} export function StoreProvider({ children }: { children: ReactNode }) { useEffect(() => { -- 2.54.0