authorgravatar for cjnwob@gmail.comChukwudi Nwobodo <cjnwob@gmail.com> 2026-03-08 06:22:02+00:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-09 14:02:29-07:00
log7a377fc5e4d607a475a7d36957689562d5dcccb9
tree8aa6767ba647bed6003162e2e8d4299df49ab80c
parent90e0c2ab15e7034d8e6e0688b3996fd905c03017

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.

1 files changed, 32 insertions(+), 4 deletions(-)

apps/web/src/store.ts+32-4
......@@ -63,6 +63,8 @@ function readPersistedState(): AppState {
6363 }
6464}
6565
66let legacyKeysCleanedUp = false;
67
6668function persistState(state: AppState): void {
6769 if (typeof window === "undefined") return;
6870 try {
......@@ -74,14 +76,29 @@ function persistState(state: AppState): void {
7476 .map((project) => project.cwd),
7577 }),
7678 );
77 for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) {
78 window.localStorage.removeItem(legacyKey);
79 if (!legacyKeysCleanedUp) {
80 legacyKeysCleanedUp = true;
81 for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) {
82 window.localStorage.removeItem(legacyKey);
83 }
7984 }
8085 } catch {
8186 // Ignore quota/storage errors to avoid breaking chat UX.
8287 }
8388}
8489
90let persistTimer: ReturnType<typeof setTimeout> | null = null;
91
92function debouncedPersistState(state: AppState): void {
93 if (persistTimer !== null) {
94 clearTimeout(persistTimer);
95 }
96 persistTimer = setTimeout(() => {
97 persistTimer = null;
98 persistState(state);
99 }, 500);
100}
101
85102// ── Pure helpers ──────────────────────────────────────────────────────
86103
87104function updateThread(
......@@ -395,8 +412,19 @@ export const useStore = create<AppStore>((set) => ({
395412 set((state) => setThreadBranch(state, threadId, branch, worktreePath)),
396413}));
397414
398// Persist on every state change
399useStore.subscribe((state) => persistState(state));
415// Persist state changes with debouncing to avoid localStorage thrashing
416useStore.subscribe((state) => debouncedPersistState(state));
417
418// Flush pending writes synchronously before page unload to prevent data loss.
419if (typeof window !== "undefined") {
420 window.addEventListener("beforeunload", () => {
421 if (persistTimer !== null) {
422 clearTimeout(persistTimer);
423 persistTimer = null;
424 persistState(useStore.getState());
425 }
426 });
427}
400428
401429export function StoreProvider({ children }: { children: ReactNode }) {
402430 useEffect(() => {