From 15b524404b12ea99809f98ed8ae887ecc6e2b757 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 7 Aug 2026 16:16:04 +0800 Subject: [PATCH] perf(editor): defer search match count past panel open animation --- .../components/editor/EditorSearchPanel.vue | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/components/editor/EditorSearchPanel.vue b/apps/desktop/src/components/editor/EditorSearchPanel.vue index 50fa8a580..4d0187b7f 100644 --- a/apps/desktop/src/components/editor/EditorSearchPanel.vue +++ b/apps/desktop/src/components/editor/EditorSearchPanel.vue @@ -35,9 +35,25 @@ let searchScopeTo: number | null = null; const inSelectionScope = ref(false); const SEARCH_UPDATE_DELAY_MS = 120; +// When the panel is opening, push the first match-count pass past the enter +// transition (150ms) so the O(document) count does not contend with the +// animation's first frames and cause dropped frames on large documents. +const SEARCH_OPEN_DELAY_MS = 200; const DOCUMENT_SEARCH_UPDATE_DELAY_MS = 500; let searchUpdateTimer: ReturnType | null = null; let documentSearchUpdateTimer: ReturnType | null = null; +let pendingIdleHandle: number | null = null; + +// requestIdleCallback lets the match-count pass yield to the animation frame +// budget; fall back to setTimeout where the API is unavailable (older webviews). +const requestIdle = typeof window !== "undefined" && typeof window.requestIdleCallback === "function" ? (cb: () => void) => window.requestIdleCallback(cb, { timeout: 500 }) : (cb: () => void) => window.setTimeout(cb, 0) as unknown as number; +const cancelIdle = typeof window !== "undefined" && typeof window.cancelIdleCallback === "function" ? (handle: number) => window.cancelIdleCallback(handle) : (handle: number) => window.clearTimeout(handle); + +function clearPendingIdle() { + if (pendingIdleHandle == null) return; + cancelIdle(pendingIdleHandle); + pendingIdleHandle = null; +} function searchMatchLimit(): number { return settingsStore.editorSettings.regexMaxMatchCount; @@ -213,8 +229,9 @@ function updateMatchInfo(autoSelect = false) { } } -function scheduleSearchUpdate(autoSelect = false) { +function scheduleSearchUpdate(autoSelect = false, delay = SEARCH_UPDATE_DELAY_MS) { clearDocumentSearchUpdate(); + clearPendingIdle(); if (searchUpdateTimer) { clearTimeout(searchUpdateTimer); searchUpdateTimer = null; @@ -227,16 +244,25 @@ function scheduleSearchUpdate(autoSelect = false) { dispatchSearchQuery(); searchUpdateTimer = setTimeout(() => { searchUpdateTimer = null; - updateMatchInfo(autoSelect); - }, SEARCH_UPDATE_DELAY_MS); + // Run the O(document) match count on an idle tick so it does not block + // the enter transition's animation frames or typing responsiveness. + pendingIdleHandle = requestIdle(() => { + pendingIdleHandle = null; + updateMatchInfo(autoSelect); + }); + }, delay); } function scheduleDocumentSearchUpdate() { if (!searchVisible.value || !searchText.value) return; clearDocumentSearchUpdate(); + clearPendingIdle(); documentSearchUpdateTimer = setTimeout(() => { documentSearchUpdateTimer = null; - updateMatchInfo(); + pendingIdleHandle = requestIdle(() => { + pendingIdleHandle = null; + updateMatchInfo(); + }); }, DOCUMENT_SEARCH_UPDATE_DELAY_MS); } @@ -265,7 +291,7 @@ function openSearch(): boolean { searchInputRef.value?.focus(); searchInputRef.value?.select(); }); - if (searchText.value) scheduleSearchUpdate(true); + if (searchText.value) scheduleSearchUpdate(true, SEARCH_OPEN_DELAY_MS); return true; } @@ -284,6 +310,7 @@ function closeSearch() { searchVisible.value = false; showReplace.value = false; clearDocumentSearchUpdate(); + clearPendingIdle(); searchScopeFrom = null; searchScopeTo = null; inSelectionScope.value = false; @@ -438,6 +465,7 @@ watch(replaceText, () => { onBeforeUnmount(() => { clearDocumentSearchUpdate(); + clearPendingIdle(); if (searchUpdateTimer) { clearTimeout(searchUpdateTimer); searchUpdateTimer = null; @@ -453,7 +481,7 @@ defineExpose({