83207: debounce autocomplete query and memoize search options#86925
83207: debounce autocomplete query and memoize search options#86925abbasifaizan70 wants to merge 44 commits into
Conversation
Codecov Report❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.
|
|
@aimane-chnaif Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4e8ba5a482
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
PR doesn’t need product input as a performance PR. Unassigning and unsubscribing myself. |
|
Please merge main |
|
@aimane-chnaif Update with the latest main. Thanks |
PR ReviewAll 30 CI checks pass. No merge conflicts detected ( Looks Good
Action needed
No blocking code issues. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad06633a9f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (!handleSearch || !autocompleteQueryWithoutFilters) { | ||
| return; | ||
| } | ||
|
|
||
| handleSearch(autocompleteQueryWithoutFilters); |
There was a problem hiding this comment.
Restore debounce before calling handleSearch
Calling handleSearch directly in this effect now fires a server search on every autocompleteQueryValue change, but SearchAutocompleteList is also used by SearchPageInputWide and SearchPageInputNarrow, where useSearchPageInput updates autocompleteQueryValue on each keystroke. That means the search page path (SearchPage.tsx → searchInServer) now issues/cancels network requests and toggles loading state per keypress instead of using the prior debounce window, which can cause request churn and UI flicker for fast typers or high-latency connections.
Useful? React with 👍 / 👎.
Regression Analysis
1. Flash of empty content during debounce window (known, mitigated)During the 300ms debounce window after the first keystroke, Result: The user sees an empty list for up to 300ms on the first keystroke before results appear. The old code showed stale recent chats during this window. This was flagged by 2. Focus resets on every keystroke instead of per-debounce
Risk: Low. Users typically don't arrow-navigate mid-word. But if they do, their arrow-key position gets reset on the next character. 3. Fast-Enter during debounce windowThe submit guard checks Risk: Low. The guard is protective, and the edge case requires arrow-keying + Enter within the debounce window. 4.
|
|
@MelvinBot find performance regressions for high traffic accounts |
Performance Regression Analysis for High-Traffic AccountsTL;DR: No hard regressions in computation cost. One structural concern: the PR introduces 1 extra re-render per keystroke of Finding 1: Extra per-keystroke re-render (main concern)Before: After: The expensive Suggestion: Consider whether Finding 2: Server search debounce reduced from 2 layers to 1 (low risk)The internal Finding 3:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad06633a9f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@aimane-chnaif Solved the above regression issue pointed out by AI feedback. |
|
Please merge main |
|
@aimane-chnaif I updated the branch with the latest main. |
|
@abbasifaizan70 is this expected? There's delay in showing results (only "Ask Concierge" shows instantly) this branch: Screen.Recording.2026-05-21.at.5.53.40.PM.movmain: Screen.Recording.2026-05-21.at.5.54.20.PM.mov |
|
@aimane-chnaif Yes, that's expected. The "Ask Concierge"/typed query row uses the immediate input value, so it renders instantly. The recent chats/autocomplete list is gated by the 300ms debounce (SEARCH_OPTION_LIST_DEBOUNCE_TIME) — that's the optimization preventing the heavy filtering from running on every keystroke. After the recent fix, we also avoid showing stale rows from the previous query during that window, so for a brief moment, only the header rows are visible. |
OverviewThe PR addresses a JS-thread freeze when typing in mobile search (issue #83207). On
The structural debouncing approach is sound and is the right fix. However, there is one regression and a couple of accuracy/quality issues. 🔴 Correctness — likely regression for the Search page header
// before: debounceHandleSearch = useDebounce(..., SEARCH_OPTION_LIST_DEBOUNCE_TIME)
useEffect(() => {
if (!handleSearch || !autocompleteQueryWithoutFilters) return;
handleSearch(autocompleteQueryWithoutFilters);
}, [autocompleteQueryWithoutFilters, handleSearch]);This is safe for Consequences for the full-page search header:
Recommendation: keep the debounce inside 🟠
|
|
@abbasifaizan70 Original issue was reported in search router only (🔍 icon). Any reason for affecting search autocomplete (Spend page) as well? |
|
@aimane-chnaif The Spend page input goes through the same SearchAutocompleteList + isSearchStringMatch code paths under the hood, so the regex pre-compilation in OptionsListUtils benefits both surfaces — that one is pure perf, no behavior change. The actual behavior change (debounce + stale-result suppression) is opt-in via the new inputQueryValue prop. SearchRouter is the only caller passing it; useSearchPageInput (Spend page) does not pass it, so the Spend page path stays exactly the same as before — just slightly faster. |
Review🤖 One likely regression worth confirming before merge, plus a couple of minor notes. The 🔴 Removing the internal
|
|
Codex Review: Didn't find any major issues. What shall we delve into next? ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
|
Please check review comments above. And conflict |
|
@MelvinBot I re-added the local useDebounce around handleSearch in SearchAutocompleteList. The Spend page (useSearchPageInput) doesn't pass inputQueryValue and isn't debounced upstream, so without it searchInServer was firing per keystroke. SearchRouter still works correctly since cancelPendingRequests coalesces the back-to-back debounced calls. |
|
@aimane-chnaif Updated, Can u check again? Thanks |
Explanation of Change
The
SearchRouterwas passingautocompleteQueryValue(updated viauseStateon every keystroke) directly toSearchAutocompleteList, triggering synchronousgetSearchOptions()+combineOrderingOfReportsAndPersonalDetails()+isSearchStringMatch()on every character typed, freezing the JS thread on mobile.Structural optimizations (debouncing + memoization prevent redundant work):
useDebouncedStateforautocompleteQueryValue: ReplacesuseStateso the expensiveSearchAutocompleteListfiltering only runs after the user pauses typing, not on every keystroke. The immediate value still drives arrow-key navigation and contextual logic.useMemoforsearchOptions: WrapsgetSearchOptions()so it only re-runs when its 13 dependencies actually change, instead of re-computing on every render.useMemoforrecentReportsOptions: WrapscombineOrderingOfReportsAndPersonalDetails()so sorting/filtering is skipped whenautocompleteQueryValueandsearchOptionshaven't changed.Algorithmic improvement:
isSearchStringMatch:new RegExp()per word per item → compile once per call, reuse across all itemsisSearchStringMatch:matchingflag withcontinuepast failures → earlyreturn falseon first mismatchsearchWords:new Set()keeping empty strings →Array.from(new Set(...)).filter(Boolean)Performance (Reassure, 40K reports):
main(baseline)getFilteredOptionswith searchfilterAndOrderOptions(40K items, 3-word query)Fixed Issues
$ #83207
PROPOSAL: #83207 (comment)
Tests
Offline tests
Same as tests.
QA Steps
Same as tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectioncanBeMissingparam foruseOnyxtoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Screen.Recording.2026-04-02.at.4.04.17.AM.mov
Android: mWeb Chrome
Screen.Recording.2026-04-02.at.4.05.33.AM.mov
iOS: Native
Screen.Recording.2026-04-02.at.3.57.50.AM.mov
iOS: mWeb Safari
Screen.Recording.2026-04-02.at.3.58.48.AM.mov
MacOS: Chrome / Safari
Screen.Recording.2026-04-02.at.3.56.08.AM.mov