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.
Unified · Split
| @@ -62,6 +62,8 @@ function readPersistedState(): AppState { · 61 unmodified lines | ||
| 62 | 62 | } |
| 63 | 63 | } |
| 64 | 64 | |
| 65 | +let legacyKeysCleanedUp = false; | |
| 66 | + | |
| 65 | 67 | function persistState(state: AppState): void { |
| 66 | 68 | if (typeof window === "undefined") return; |
| 67 | 69 | try { |
| @@ -73,14 +75,29 @@ function persistState(state: AppState): void { · 5 unmodified lines | ||
| 73 | 75 | .map((project) => project.cwd), |
| 74 | 76 | }), |
| 75 | 77 | ); |
| 76 | − for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { | |
| 77 | − window.localStorage.removeItem(legacyKey); | |
| 78 | + if (!legacyKeysCleanedUp) { | |
| 79 | + legacyKeysCleanedUp = true; | |
| 80 | + for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { | |
| 81 | + window.localStorage.removeItem(legacyKey); | |
| 82 | + } | |
| 78 | 83 | } |
| 79 | 84 | } catch { |
| 80 | 85 | // Ignore quota/storage errors to avoid breaking chat UX. |
| 81 | 86 | } |
| 82 | 87 | } |
| 83 | 88 | |
| 89 | +let persistTimer: ReturnType<typeof setTimeout> | null = null; | |
| 90 | + | |
| 91 | +function 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 | 101 | // ── Pure helpers ────────────────────────────────────────────────────── |
| 85 | 102 | |
| 86 | 103 | function updateThread( |
| @@ -394,8 +411,19 @@ export const useStore = create<AppStore>((set) => ({ · 307 unmodified lines | ||
| 394 | 411 | set((state) => setThreadBranch(state, threadId, branch, worktreePath)), |
| 395 | 412 | })); |
| 396 | 413 | |
| 397 | −// Persist on every state change | |
| 398 | −useStore.subscribe((state) => persistState(state)); | |
| 414 | +// Persist state changes with debouncing to avoid localStorage thrashing | |
| 415 | +useStore.subscribe((state) => debouncedPersistState(state)); | |
| 416 | + | |
| 417 | +// Flush pending writes synchronously before page unload to prevent data loss. | |
| 418 | +if (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 | |
| 400 | 428 | export function StoreProvider({ children }: { children: ReactNode }) { |
| 401 | 429 | useEffect(() => { |