From 6f6abf1d38e585749dcbb862310ecaaced5199c9 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 10 Sep 2026 20:33:45 +0800 Subject: [PATCH] perf(tabs): debounce UI state mementos --- .../src/lib/__tests__/tabs/tabUiState.spec.ts | 9 +++++-- apps/desktop/src/lib/tabs/tabUiState.ts | 24 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/tabs/tabUiState.spec.ts b/apps/desktop/src/lib/__tests__/tabs/tabUiState.spec.ts index 6371531954..e4b219bea1 100644 --- a/apps/desktop/src/lib/__tests__/tabs/tabUiState.spec.ts +++ b/apps/desktop/src/lib/__tests__/tabs/tabUiState.spec.ts @@ -1,8 +1,8 @@ /** @vitest-environment happy-dom */ import { createApp, defineComponent, h, nextTick, ref } from "vue"; -import { afterEach, describe, expect, it } from "vitest"; -import { MAX_TAB_PAGE_UI_STATE_BYTES, provideTabUiState, sanitizeTabPageUiState, sanitizeTabUiState, useTabUiState } from "@/lib/tabs/tabUiState"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { MAX_TAB_PAGE_UI_STATE_BYTES, provideTabUiState, sanitizeTabPageUiState, sanitizeTabUiState, TAB_UI_STATE_TRACK_DEBOUNCE_MS, useTabUiState } from "@/lib/tabs/tabUiState"; const mounted: Array<{ unmount: () => void; host: HTMLElement }> = []; @@ -11,10 +11,12 @@ afterEach(() => { unmount(); host.remove(); } + vi.useRealTimers(); }); describe("tabUiState", () => { it("restores a namespace and flushes its last value before unmount", async () => { + vi.useFakeTimers(); const updates: unknown[] = []; const trackedValue = ref(0); let initialValue = 0; @@ -53,6 +55,9 @@ describe("tabUiState", () => { trackedValue.value = 8; await nextTick(); + expect(updates).toHaveLength(0); + vi.advanceTimersByTime(TAB_UI_STATE_TRACK_DEBOUNCE_MS); + await nextTick(); expect(updates.at(-1)).toEqual({ Panel: { value: 8 } }); trackedValue.value = 9; diff --git a/apps/desktop/src/lib/tabs/tabUiState.ts b/apps/desktop/src/lib/tabs/tabUiState.ts index 6f2bb130c8..cf95648b29 100644 --- a/apps/desktop/src/lib/tabs/tabUiState.ts +++ b/apps/desktop/src/lib/tabs/tabUiState.ts @@ -2,6 +2,7 @@ import { inject, onBeforeUnmount, provide, watch, type InjectionKey } from "vue" import type { TabPageUiState, TabUiState } from "@/types/database"; export const MAX_TAB_PAGE_UI_STATE_BYTES = 64 * 1024; +export const TAB_UI_STATE_TRACK_DEBOUNCE_MS = 100; interface TabUiStateContext { snapshot: TabPageUiState; @@ -28,11 +29,28 @@ export function useTabUiState( const update = (patch: Partial) => { if (active) context?.update(namespace ? { [namespace]: patch } : (patch as TabPageUiState)); }; - const track = (snapshot: () => Partial) => { - const stop = watch(snapshot, update, { deep: true, flush: "post" }); - onBeforeUnmount(() => { + const track = (snapshot: () => Partial, debounceMs = TAB_UI_STATE_TRACK_DEBOUNCE_MS) => { + let timer: ReturnType | undefined; + const flush = () => { + if (timer !== undefined) { + clearTimeout(timer); + timer = undefined; + } update(snapshot()); + }; + const schedule = () => { + if (!active) return; + if (timer !== undefined) clearTimeout(timer); + if (debounceMs <= 0) { + flush(); + return; + } + timer = setTimeout(flush, debounceMs); + }; + const stop = watch(snapshot, schedule, { deep: true, flush: "post" }); + onBeforeUnmount(() => { stop(); + flush(); active = false; }); };