Skip to content

Make Switching to the Search Page Faster#74108

Merged
mountiny merged 27 commits into
Expensify:mainfrom
software-mansion-labs:feature/kuba-nowakowski/preload_reports_v2
Nov 24, 2025
Merged

Make Switching to the Search Page Faster#74108
mountiny merged 27 commits into
Expensify:mainfrom
software-mansion-labs:feature/kuba-nowakowski/preload_reports_v2

Conversation

@sumo-slonik

@sumo-slonik sumo-slonik commented Nov 3, 2025

Copy link
Copy Markdown
Contributor

Explanation of Change

Thanks to cleaning up the structure of the search page component, we were able to speed up the time it takes to switch to the Search tab. Below are metrics from several platforms as an example of the time gains:

Desktop web : 2.1s → 1.1s

iOS native ‎ : 6,04s → 4.5s

This shows that we’re managing to save roughly half of the time previously needed to switch tabs on the web, and also more than 1 second on the native platform.

dekompozycja.mp4

Fixed Issues

$ #74613
PROPOSAL:

Tests

  1. Go to the Search tab.

  2. Check whether the results are displayed correctly.

  3. Make sure you can correctly open all the filters

  4. Verify you can select multiple expense / reports and open the options for bulk actions

  • Verify that no errors appear in the JS console

Offline tests

unnesesary

QA Steps

Same as test
// TODO: These must be filled out, or the issue title must include "[No QA]."

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
    • MacOS: Desktop
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
Screen.Recording.2025-11-14.at.15.55.21.mov
iOS: Native
Screen.Recording.2025-11-14.at.15.15.02.mov
iOS: mWeb Safari
Screen.Recording.2025-11-14.at.15.04.23.mov
MacOS: Chrome / Safari
dekompozycja.1.mov
MacOS: Desktop
Screen.Recording.2025-11-14.at.16.07.07.mov

});

try {
debugger;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this debugger statement before merging. Debugger statements should not be committed to the codebase as they can cause the application to pause execution in production.

@github-actions

github-actions Bot commented Nov 3, 2025

Copy link
Copy Markdown
Contributor

LGTM :feelsgood:. Thank you for your hard work!

@github-actions

github-actions Bot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

🚧 @mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

@mountiny

mountiny commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

I know this is still a draft but for me no Search API call is made
https://github.com/user-attachments/assets/f7634169-7136-46c3-9250-b8c9c3e76603

@sumo-slonik sumo-slonik force-pushed the feature/kuba-nowakowski/preload_reports_v2 branch from 428772a to 0401f08 Compare November 12, 2025 10:17
# Conflicts:
#	src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts
@codecov

codecov Bot commented Nov 12, 2025

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
...ion/AppNavigator/usePreloadFullScreenNavigators.ts 17.24% <100.00%> (ø)
src/pages/Search/SearchPageWide.tsx 8.33% <8.33%> (ø)
src/pages/Search/SearchPage.tsx 26.97% <40.54%> (+2.75%) ⬆️
... and 19 files with indirect coverage changes

@github-actions

Copy link
Copy Markdown
Contributor

🚧 @mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

Copy link
Copy Markdown
Contributor

🧪🧪 Use the links below to test this adhoc build on Android, iOS, Desktop, and Web. Happy testing! 🧪🧪
Built from App PR #74108.

Android 🤖 iOS 🍎
https://ad-hoc-expensify-cash.s3.amazonaws.com/android/74108/index.html https://ad-hoc-expensify-cash.s3.amazonaws.com/ios/74108/index.html
Android iOS
Desktop 💻 Web 🕸️
https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/74108/NewExpensify.dmg https://74108.pr-testing.expensify.com
Desktop Web

👀 View the workflow run that generated this build 👀

Comment thread src/pages/Search/SearchPage.tsx Outdated
return;
}
handleSearchAction({queryJSON, searchKey, offset: 0, shouldCalculateTotals});
// eslint-disable-next-line react-compiler/react-compiler

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's not yet enforced anywhere, but we really shouldn't integrate any new code that disables the compiler, especially so high up in the components' tree. Can you rethink how this can be implemented following the rules of React?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we should follow that approach but in this particular case I don’t think it will make much difference because the SearchPage already has a lot of issues with the React compiler even without this useEffect, which means it isn’t being compiled anyway. So adding or removing this useEffect won’t really change that situation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After testing, I think I’ll do it the way you suggested and make sure it doesn’t conflict with the compiler.

// Currently, only the Account and Workspaces tabs are preloaded. The remaining tabs will be supported soon.
const TABS_TO_PRELOAD = [NAVIGATION_TABS.SETTINGS, NAVIGATION_TABS.WORKSPACES];
// Currently, only the Inbox, Workspaces, Account tabs are preloaded. The remaining tabs will be supported soon.
const TABS_TO_PRELOAD = [NAVIGATION_TABS.HOME, NAVIGATION_TABS.SEARCH, NAVIGATION_TABS.WORKSPACES, NAVIGATION_TABS.SETTINGS];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the PR, this is the only place where we actually turn on the preloading logic. Given that, why can't it be integrated on it's own as a separate change?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to call out the docs for navigation.preload:

Preloading a screen means that the screen will be rendered in the background. All the components in the screen will be mounted and the useEffect hooks will be called.

As much as it comes with a good visual effect in isolated testing, at scale this can behave much like a heavy React Context - kill the general JS performance by triggering a lot of the logic in the background, in a much less predictable way. I see (and like) the direction of another part of this PR where we disable the tree when it's not needed, but I'd really emphasize on the importance of performance testing with large accounts before we integrate preloading for this heavy screen (and others).

Search just does a lot of the heavy lifting, and doing this heavy work at a different time (also instantiating effects) can potentially cause us harm, one that is super hard to track, debug and fix.

Preloading itself might yield good results when we want to keep a cheap screen in the memory, but is not a general optimization strategy for really complex (and alive) screens.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes I added were also required for enabling preloading by splitting the SearchPage into smaller components, it became easier to preload only the skeletons. Thanks to that, the entire heavy screen with all search results is not kept in memory, only the lightweight skeletons are.

During preloading, useEffect and useOnyx hooks are still executed, but the actual search is not triggered since it only runs when isFocused is true. This approach helps slightly reduce the issue you mentioned regarding keeping a heavy screen in memory.

Additionally, together with @WojtekBoman, we’re planning to adjust the preloading logic so that not all screens are preloaded at once. We want to spread this over time to optimize performance, but that will require a separate PR.

Given all that, I think keeping preloading here is still worth it despite the trade-offs involved.

import {openOldDotLink} from '@userActions/Link';
import CONST from '@src/CONST';

type SearchModalsWrapperProps = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this interface and the amount of responsibilities within the component, this is the very opposite of writing composable React. This will only grow in size, is error prone and hard to maintain from the very start. Can you explain why it has to be implemented (and why this way) for the preloading?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s not required specifically because of preloading the main reason was that these components were repeatedly used in SearchPage, which had over 1000 lines of code. I figured that any opportunity to make it a bit lighter and more modular was worth taking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you feel that this wrapper is unnecessary, I can remove it, but in my opinion, it simplifies the structure of the SearchPage itself, which is already huge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I think this is something that we'll eventually aim to avoid, so I'd go ahead and remove it. If the structure it complex, it's definitely not a viable solution to create a giga wrapper that is a prop sink for every piece of functionality that lives in there.

Let's focus on the gains coming from not rendering parts of the UI - the rest we can solve later by better composing the screens.

Comment thread src/pages/Search/SearchPage.tsx Outdated

const {accountID} = useCurrentUserPersonalDetails();
const {defaultCardFeed} = useCardFeedsForDisplay();
const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My Chrome tab couldn't record a performance profile for me when working with this branch on the exfy-perf@callstack.com account (Applause workspace). I can't tell for sure, but might this be due to these calculations?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI I had the same by only preloading the Search screen, no additional changes (for this account).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the difficulty in observing this in the profiler may be due to the calculations performed during preloading. That’s why I chose to include a screen recording rather than rely solely on metrics , even if the metrics don’t show a significant difference, the user experience is noticeably improved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to do some research, but it’s possible that this is due to the time required by passive effects.

Comment thread src/pages/Search/SearchPage.tsx Outdated
shouldShowFooter={shouldShowFooter}
/>
)}
{(!shouldUseNarrowLayout || isMobileSelectionModeEnabled) && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This references my previous comment. I see a lot of the changes in the PR could be decoupled from the preloading itself - it looks like much of the performance gain might be coming from disabling a part of the React tree for the initial load. Maybe it can be integrated separately?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that some of the changes aren’t directly related to preloading, but splitting this into two separate PRs might not be the best idea either. Some of these changes were aimed at fixing bugs that appeared during preloading, such as blank flickering screens or results not being displayed, and at the same time, refactoring this component was necessary to properly store the skeletons as a preloaded screen.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adhorodyski I’ll double-check with more tests, but it seems that decomposition brings most of the benefit, while preload adds less and might not be worth including. Removing preload bugs solutions also improves results, so decomposition alone already looks good. I’ll share detailed results tomorrow.

@sumo-slonik

Copy link
Copy Markdown
Contributor Author
image

Fixed!

@linhvovan29546

linhvovan29546 commented Nov 21, 2025

Copy link
Copy Markdown
Contributor
image Fixed!

I don't see the offline indicator on the wide screen. Could you please check again?

@sumo-slonik

Copy link
Copy Markdown
Contributor Author
image Fixed!

I don't see the offline indicator on the wide screen. Could you please check again?

Yes, it’s not there, but I did it on purpose because it seems to me that on the main wide screen we don’t add it either, so I assumed this is working as designed.

@linhvovan29546

linhvovan29546 commented Nov 21, 2025

Copy link
Copy Markdown
Contributor

Yes, it’s not there

If that doesn't appear, then it's a bug.

Screenshot 2025-11-21 at 21 53 23

@sumo-slonik

Copy link
Copy Markdown
Contributor Author

Yes, it’s not there

If that doesn't appear, then it's a bug.

It’s already fixed. You were right it wasn’t showing up for me because of the data I had cached locally. After clearing the cache, it displayed correctly on main. It should be all good now.

@mountiny

Copy link
Copy Markdown
Contributor

@linhvovan29546 how is this looking?

Comment thread src/pages/Search/SearchPageWide.tsx
@linhvovan29546

Copy link
Copy Markdown
Contributor

@linhvovan29546 how is this looking?

Only one minor UI bug left

@linhvovan29546

Copy link
Copy Markdown
Contributor

I’ve tested across all platforms. Only one minor UI bug remains. I’ll re-review once the conflict is resolved

@sumo-slonik

Copy link
Copy Markdown
Contributor Author

I think I’ve fixed everything. If you think it’s all good now, I’ll move on to resolving the conflicts.

@linhvovan29546

Copy link
Copy Markdown
Contributor

Yeah please resolve the conflicts

# Conflicts:
#	src/pages/Search/SearchPage.tsx
#	src/pages/Search/SearchPageNarrow.tsx
@adhorodyski

Copy link
Copy Markdown
Contributor

Reviewing now.

Comment on lines +622 to -632
lastPaymentMethods,
selectedReportIDs,
selectedTransactionReportIDs,
queryJSON,
selectedPolicyIDs,
policies,
integrationsExportTemplates,
csvExportLayouts,
clearSelectedTransactions,
lastPaymentMethods,
beginExportWithTemplate,
bulkPayButtonOptions,
onBulkPaySelected,
theme.icon,
styles.colorMuted,
styles.fontWeightNormal,
styles.textWrap,
beginExportWithTemplate,
integrationsExportTemplates,
csvExportLayouts,
policies,
bulkPayButtonOptions,
onBulkPaySelected,
selectedPolicyIDs,
selectedReportIDs,
selectedTransactionReportIDs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In future, it would be great to avoid any changes like this that just increase the diff of the pr and make it harder to review

Comment on lines +187 to +194
queueExportSearchWithTemplate({
templateName,
templateType,
jsonQuery: JSON.stringify(queryJSON),
reportIDList: [],
transactionIDList: [],
policyID,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here as well as far as I can see is no change, maybe if this was a small PR, then feel free to make style changes, but in large prs like this one, it is counter productive 🙌

Comment on lines +27 to +48
queryJSON?: SearchQueryJSON;
searchResults: OnyxEntry<SearchResults>;
searchRequestResponseStatusCode: number | null;
isMobileSelectionModeEnabled: boolean;
headerButtonsOptions: Array<DropdownOption<SearchHeaderOptionValue>>;
footerData: {
count: number | undefined;
total: number | undefined;
currency: string | undefined;
};
selectedPolicyIDs: Array<string | undefined>;
selectedTransactionReportIDs: string[];
selectedReportIDs: string[];
latestBankItems?: BankAccountMenuItem[];
onBulkPaySelected: (paymentMethod?: PaymentMethodType) => void;
handleSearchAction: (value: SearchParams | string) => void;
onSortPressedCallback: () => void;
scrollHandler: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
initScanRequest: (e: DragEvent) => void;
PDFValidationComponent: React.ReactNode;
ErrorModal: React.ReactNode;
shouldShowFooter: boolean;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these should have docs, can you add them in a follow up please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll take care of it once I’m done with the inbox.

@mountiny mountiny marked this pull request as draft November 24, 2025 15:36
@mountiny mountiny marked this pull request as ready for review November 24, 2025 15:36
@mountiny mountiny merged commit ae86bcb into Expensify:main Nov 24, 2025
35 checks passed
@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.2.63-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/marcaaron in version: 9.2.63-8 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

shouldShowLink={false}
>
{!!queryJSON && (
<>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this PR we accidentally missed DragAndDropProvider, which led to issue #76986. We've applied the fix here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants