fix(omnibox): debounce search with min 2-char threshold to prevent freeze
- commit
- f4caba401e38234c7decb64dd18ff87c4160ab02
- author
- bnema · · 2025-11-29 12:34 UTC
- committer
- bnema · · 2025-11-29 13:24 UTC
- parent
- 99c61d9
- signature
- unsigned
3 files · +53 -10
M CHANGELOG.md +1 -0
| @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. | |||
| 15 | 15 | - **URL normalization**: Consistent URL storage across browser, CLI, and favicon services (strips trailing slashes, normalizes schemes). | |
| 16 | 16 | ||
| 17 | 17 | ### Fixed | |
| 18 | + | - **Omnibox search performance**: Debounce search with 2-character minimum threshold to prevent UI freeze on first keystroke. Applies to both URL/history search and find-in-page modes. | |
| 18 | 19 | - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation. | |
| 19 | 20 | - **Zoom toast suppression**: Suppress zoom toast notifications during browser initialization to prevent UI flash. | |
| 20 | 21 | - **Cosmetic filter injection**: Inject rules immediately and fix selector normalization for better ad blocking. | |
| @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. | |||||
| 15 | - **URL normalization**: Consistent URL storage across browser, CLI, and favicon services (strips trailing slashes, normalizes schemes). | 15 | - **URL normalization**: Consistent URL storage across browser, CLI, and favicon services (strips trailing slashes, normalizes schemes). | ||
| 16 | 16 | ||||
| 17 | ### Fixed | 17 | ### Fixed | ||
| 18 | + | - **Omnibox search performance**: Debounce search with 2-character minimum threshold to prevent UI freeze on first keystroke. Applies to both URL/history search and find-in-page modes. | |||
| 18 | - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation. | 19 | - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation. | ||
| 19 | - **Zoom toast suppression**: Suppress zoom toast notifications during browser initialization to prevent UI flash. | 20 | - **Zoom toast suppression**: Suppress zoom toast notifications during browser initialization to prevent UI flash. | ||
| 20 | - **Cosmetic filter injection**: Inject rules immediately and fix selector normalization for better ad blocking. | 21 | - **Cosmetic filter injection**: Inject rules immediately and fix selector normalization for better ad blocking. | ||
M gui/src/components/omnibox/OmniboxInput.svelte +3 -3
| @@ -5,7 +5,7 @@ | |||
| 5 | 5 | --> | |
| 6 | 6 | <script lang="ts"> | |
| 7 | 7 | import { omniboxStore } from './stores.svelte.ts'; | |
| 8 | - | import { debouncedQuery, omniboxBridge } from './messaging'; | |
| 8 | + | import { debouncedQuery, debouncedFind, omniboxBridge } from './messaging'; | |
| 9 | 9 | import { findInPage } from './find'; | |
| 10 | 10 | ||
| 11 | 11 | // Reactive state | |
| @@ -131,8 +131,8 @@ | |||
| 131 | 131 | } | |
| 132 | 132 | } | |
| 133 | 133 | } else if (mode === 'find') { | |
| 134 | - | // Immediate find for search | |
| 135 | - | findInPage(value); | |
| 134 | + | // Debounced find with minimum length to avoid freeze on first letter | |
| 135 | + | debouncedFind(value, findInPage); | |
| 136 | 136 | } | |
| 137 | 137 | } | |
| 138 | 138 | ||
| @@ -5,7 +5,7 @@ | |||||
| 5 | --> | 5 | --> | ||
| 6 | <script lang="ts"> | 6 | <script lang="ts"> | ||
| 7 | import { omniboxStore } from './stores.svelte.ts'; | 7 | import { omniboxStore } from './stores.svelte.ts'; | ||
| 8 | - | import { debouncedQuery, omniboxBridge } from './messaging'; | 8 | + | import { debouncedQuery, debouncedFind, omniboxBridge } from './messaging'; |
| 9 | import { findInPage } from './find'; | 9 | import { findInPage } from './find'; | ||
| 10 | 10 | ||||
| 11 | // Reactive state | 11 | // Reactive state | ||
| @@ -131,8 +131,8 @@ | |||||
| 131 | } | 131 | } | ||
| 132 | } | 132 | } | ||
| 133 | } else if (mode === 'find') { | 133 | } else if (mode === 'find') { | ||
| 134 | - | // Immediate find for search | 134 | + | // Debounced find with minimum length to avoid freeze on first letter |
| 135 | - | findInPage(value); | 135 | + | debouncedFind(value, findInPage); |
| 136 | } | 136 | } | ||
| 137 | } | 137 | } | ||
| 138 | 138 | ||||
M gui/src/components/omnibox/messaging.ts +49 -7
| @@ -191,17 +191,59 @@ export class OmniboxBridge implements OmniboxMessageBridge { | |||
| 191 | 191 | // Singleton instance | |
| 192 | 192 | export const omniboxBridge = new OmniboxBridge(); | |
| 193 | 193 | ||
| 194 | + | // Minimum characters required before triggering search operations | |
| 195 | + | const MIN_SEARCH_LENGTH = 2; | |
| 196 | + | ||
| 197 | + | // Debounce timers for different operations | |
| 198 | + | const debounceTimers: Record<string, number> = {}; | |
| 199 | + | ||
| 194 | 200 | /** | |
| 195 | - | * Debounced query function for search input | |
| 201 | + | * Creates a debounced search function with minimum length threshold | |
| 202 | + | * Prevents freezing on first letter and rapid input | |
| 196 | 203 | */ | |
| 197 | - | export function debouncedQuery(searchTerm: string): void { | |
| 198 | - | omniboxStore.clearDebounceTimer(); | |
| 204 | + | function createDebouncedSearch( | |
| 205 | + | key: string, | |
| 206 | + | action: (query: string) => void, | |
| 207 | + | onClear?: () => void | |
| 208 | + | ): (query: string) => void { | |
| 209 | + | return (query: string) => { | |
| 210 | + | // Clear previous timer | |
| 211 | + | if (debounceTimers[key]) { | |
| 212 | + | clearTimeout(debounceTimers[key]); | |
| 213 | + | debounceTimers[key] = 0; | |
| 214 | + | } | |
| 199 | 215 | ||
| 200 | - | const timerId = window.setTimeout(() => { | |
| 201 | - | omniboxBridge.query(searchTerm); | |
| 202 | - | }, omniboxStore.config.debounceDelay); | |
| 216 | + | const trimmed = (query || "").trim(); | |
| 203 | 217 | ||
| 204 | - | omniboxStore.setDebounceTimer(timerId); | |
| 218 | + | // Clear/reset immediately if query is empty or too short | |
| 219 | + | if (trimmed.length < MIN_SEARCH_LENGTH) { | |
| 220 | + | if (onClear) { | |
| 221 | + | onClear(); | |
| 222 | + | } | |
| 223 | + | return; | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | // Debounce the actual search operation | |
| 227 | + | debounceTimers[key] = window.setTimeout(() => { | |
| 228 | + | action(query); | |
| 229 | + | }, omniboxStore.config.debounceDelay); | |
| 230 | + | }; | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | /** | |
| 234 | + | * Debounced query function for omnibox search input | |
| 235 | + | */ | |
| 236 | + | export const debouncedQuery = createDebouncedSearch( | |
| 237 | + | "query", | |
| 238 | + | (searchTerm) => omniboxBridge.query(searchTerm), | |
| 239 | + | () => omniboxStore.updateSuggestions([]) | |
| 240 | + | ); | |
| 241 | + | ||
| 242 | + | /** | |
| 243 | + | * Debounced find function for find-in-page | |
| 244 | + | */ | |
| 245 | + | export function debouncedFind(query: string, findFn: (q: string) => void): void { | |
| 246 | + | createDebouncedSearch("find", findFn, () => findFn(""))(query); | |
| 205 | 247 | } | |
| 206 | 248 | ||
| 207 | 249 | // Extend global window interface for Go bridge compatibility | |
| @@ -191,17 +191,59 @@ export class OmniboxBridge implements OmniboxMessageBridge { | |||||
| 191 | // Singleton instance | 191 | // Singleton instance | ||
| 192 | export const omniboxBridge = new OmniboxBridge(); | 192 | export const omniboxBridge = new OmniboxBridge(); | ||
| 193 | 193 | ||||
| 194 | + | // Minimum characters required before triggering search operations | |||
| 195 | + | const MIN_SEARCH_LENGTH = 2; | |||
| 196 | + | ||||
| 197 | + | // Debounce timers for different operations | |||
| 198 | + | const debounceTimers: Record<string, number> = {}; | |||
| 199 | + | ||||
| 194 | /** | 200 | /** | ||
| 195 | - | * Debounced query function for search input | 201 | + | * Creates a debounced search function with minimum length threshold |
| 202 | + | * Prevents freezing on first letter and rapid input | |||
| 196 | */ | 203 | */ | ||
| 197 | - | export function debouncedQuery(searchTerm: string): void { | 204 | + | function createDebouncedSearch( |
| 198 | - | omniboxStore.clearDebounceTimer(); | 205 | + | key: string, |
| 206 | + | action: (query: string) => void, | |||
| 207 | + | onClear?: () => void | |||
| 208 | + | ): (query: string) => void { | |||
| 209 | + | return (query: string) => { | |||
| 210 | + | // Clear previous timer | |||
| 211 | + | if (debounceTimers[key]) { | |||
| 212 | + | clearTimeout(debounceTimers[key]); | |||
| 213 | + | debounceTimers[key] = 0; | |||
| 214 | + | } | |||
| 199 | 215 | ||||
| 200 | - | const timerId = window.setTimeout(() => { | 216 | + | const trimmed = (query || "").trim(); |
| 201 | - | omniboxBridge.query(searchTerm); | |||
| 202 | - | }, omniboxStore.config.debounceDelay); | |||
| 203 | 217 | ||||
| 204 | - | omniboxStore.setDebounceTimer(timerId); | 218 | + | // Clear/reset immediately if query is empty or too short |
| 219 | + | if (trimmed.length < MIN_SEARCH_LENGTH) { | |||
| 220 | + | if (onClear) { | |||
| 221 | + | onClear(); | |||
| 222 | + | } | |||
| 223 | + | return; | |||
| 224 | + | } | |||
| 225 | + | ||||
| 226 | + | // Debounce the actual search operation | |||
| 227 | + | debounceTimers[key] = window.setTimeout(() => { | |||
| 228 | + | action(query); | |||
| 229 | + | }, omniboxStore.config.debounceDelay); | |||
| 230 | + | }; | |||
| 231 | + | } | |||
| 232 | + | ||||
| 233 | + | /** | |||
| 234 | + | * Debounced query function for omnibox search input | |||
| 235 | + | */ | |||
| 236 | + | export const debouncedQuery = createDebouncedSearch( | |||
| 237 | + | "query", | |||
| 238 | + | (searchTerm) => omniboxBridge.query(searchTerm), | |||
| 239 | + | () => omniboxStore.updateSuggestions([]) | |||
| 240 | + | ); | |||
| 241 | + | ||||
| 242 | + | /** | |||
| 243 | + | * Debounced find function for find-in-page | |||
| 244 | + | */ | |||
| 245 | + | export function debouncedFind(query: string, findFn: (q: string) => void): void { | |||
| 246 | + | createDebouncedSearch("find", findFn, () => findFn(""))(query); | |||
| 205 | } | 247 | } | ||
| 206 | 248 | ||||
| 207 | // Extend global window interface for Go bridge compatibility | 249 | // Extend global window interface for Go bridge compatibility | ||