Skip to content

Improve performance of inbox page loading#76012

Merged
luacmartins merged 26 commits intoExpensify:mainfrom
software-mansion-labs:feature/kuba-nowakowski/improve_performance_of_inbox
Jan 23, 2026
Merged

Improve performance of inbox page loading#76012
luacmartins merged 26 commits intoExpensify:mainfrom
software-mansion-labs:feature/kuba-nowakowski/improve_performance_of_inbox

Conversation

@sumo-slonik
Copy link
Contributor

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

Managed to activate the compiler in the Reports screen by stopping the existing useRef and useState that were already there, simplifying the logic. Additionally, I was able to implement memoization in other places and fix the preload of the inbox tab, which results in a 70% performance gain on web loading and 60% on mobile native for a heavy account.

Additionally, the redirect from a non-existent report has been removed, and we are now showing the "not found" state instead.

Explanation of Change

Fixed Issues

$ #71587
$ #74613
$ #73993 <- this caused the previous revert
$ #73691
PROPOSAL:

Tests

  1. Open any report.

  2. Skeleton loader should be visible while messages are being fetched.

  3. Skeleton loader should disappear once data is loaded and be replaced with report actions.

  4. If no data is fetched, there should be an empty state component with "No activity yet" information.

  5. Check whether the inbox loads correctly when navigating from each tab.

Test to verify the bug does not reproduce:

  1. Open any expense from the inbox.
  2. Go to Search and find that expense.
  3. Delete it from the Search view.
  4. Go back to the inbox.
  5. Expected result: There is no infinite loading of skeletons.

Expected result:
There is no infinite loading of skeletons.

  • Verify that no errors appear in the JS console

Offline tests

unesesary

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
  • 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
Screen.Recording.2025-12-03.at.13.36.52.mov
Android: mWeb Chrome
Screen.Recording.2025-12-03.at.13.43.07.mov
iOS: Native
Screen.Recording.2025-12-03.at.13.48.47.mov
iOS: mWeb Safari
Screen.Recording.2025-12-03.at.11.48.37.mov
MacOS: Chrome / Safari
Screen.Recording.2025-12-03.at.11.44.30.mov

@sumo-slonik
Copy link
Contributor Author

image

So far we have a 10% gain compared to the main version, and I’m continuing to work on other things that could be improved.

@codecov
Copy link

codecov bot commented Nov 25, 2025

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.

Files with missing lines Coverage Δ
.../AppNavigator/Navigators/ReportsSplitNavigator.tsx 100.00% <100.00%> (ø)
...ion/AppNavigator/usePreloadFullScreenNavigators.ts 16.12% <100.00%> (ø)
src/pages/home/hooks/useReportWasDeleted.ts 100.00% <100.00%> (ø)
...c/components/Navigation/NavigationTabBar/index.tsx 50.37% <0.00%> (-1.17%) ⬇️
src/pages/home/ReportScreen.tsx 67.74% <63.63%> (-0.50%) ⬇️
... and 15 files with indirect coverage changes

@sumo-slonik
Copy link
Contributor Author

sumo-slonik commented Nov 26, 2025

After comparing data in the Applause account, I can see that it’s possible to achieve an inbox-open gain of around 75% of the time. However, this requires combining two approaches: using useMemo and screen preloading. As shown in the attached screenshot, this delivers the best results.
But to do this, I need to solve the problem that caused the previous revert of the inbox page preloading.

image

…of_inbox

# Conflicts:
#	src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts
@sumo-slonik
Copy link
Contributor Author

I managed to get the compiler running in reportSvcrean, add memoization to several components, and activate preload, which produced the following results.

WEB:

image

IOS:
image

This kind of render-time reduction is even more noticeable for the user in real usage, thanks to the benefits of preload. These improvements are visible in practice but require computations that extend the measured render time.

This gives us roughly a 70% gain on the web and about a 60% gain on native platforms.

All measurements were taken on the Aplause account. Now I need to check whether there are any bugs, and I’ll try to fix them if they appear. But we’re on a very good path toward a massive performance improvement on this screen

@sumo-slonik
Copy link
Contributor Author

It looks like the bugs that caused the previous preload-inbox rollbacks no longer occur.

I’m now working on recording the videos and reviewing everything to make sure there isn’t a case that would still break it.

@sumo-slonik sumo-slonik force-pushed the feature/kuba-nowakowski/improve_performance_of_inbox branch from 179e0d9 to 5ac39ed Compare December 3, 2025 13:41
@sumo-slonik sumo-slonik marked this pull request as ready for review December 3, 2025 15:25
@sumo-slonik sumo-slonik requested review from a team as code owners December 3, 2025 15:25
@melvin-bot melvin-bot bot requested review from situchan and trjExpensify and removed request for a team and trjExpensify December 3, 2025 15:25
@melvin-bot
Copy link

melvin-bot bot commented Dec 3, 2025

@situchan 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]

const isFocused = useIsFocused();
const prevIsFocused = usePrevious(isFocused);
const firstRenderRef = useRef(true);
const [firstRender, setFirstRender] = useState(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain the reason why use state instead of ref?

Copy link
Contributor Author

@sumo-slonik sumo-slonik Dec 3, 2025

Choose a reason for hiding this comment

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

Thanks to replacing useRef with useState, we were able to enable the React Compiler. The benefit we previously got from useRef was minimal anyway, because we were already using setState below, which triggered a re-render. Now that ref is no longer used inside the effect, the React Compiler can properly memoize the component, resulting in better performance.

Copy link
Contributor

Choose a reason for hiding this comment

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

Though this ref was introduced 2 years ago, last usage was 10 months ago.
We need to make sure that this removal doesn't reintroduce #55251

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This bug no longer occurs because newly created expenses are now displayed in the new report view, and in that view the issue does not appear when removing expense.

Screen.Recording.2025-12-04.at.10.20.34.mov

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Screen.Recording.2025-12-04.at.10.48.17.mov

Comment on lines 27 to 28
// Currently the Inbox, Workspaces and Account tabs are preloaded, while Search is not preloaded due to its potential complexity.
const TABS_TO_PRELOAD = [NAVIGATION_TABS.HOME, NAVIGATION_TABS.SETTINGS, NAVIGATION_TABS.WORKSPACES];
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Currently the Inbox, Workspaces and Account tabs are preloaded, while Search is not preloaded due to its potential complexity.
const TABS_TO_PRELOAD = [NAVIGATION_TABS.HOME, NAVIGATION_TABS.SETTINGS, NAVIGATION_TABS.WORKSPACES];
const TABS_TO_PRELOAD = [NAVIGATION_TABS.HOME, NAVIGATION_TABS.WORKSPACES, NAVIGATION_TABS.SETTINGS];

to be consistent with visual order

const [isBannerVisible, setIsBannerVisible] = useState(true);
const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({});

const wasReportAccessibleRef = useRef(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to confirm this removal won't reintroduce #65853, #65914.

This was previously removed in #65622 but reverted due to regression above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm already checking whether this doesn't reproduce. Thanks for finding it, because I have a feeling it might show up, since I think I had checked different cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’m not sure what the correct behavior should be here. If we try to navigate to a deleted report, should we see the “Oops” screen or not?
Currently, on my branch, the screen is displayed (previously it wasn’t), but I saw an issue where “works as designed” was marked, stating that it should appear.

image

The message on the page also seems appropriate, since it informs us that we’re trying to navigate to an expense that has already been deleted.

Screen.Recording.2025-12-04.at.10.50.51.mov

It seems to me that it would be best if the @Expensify/design team spoke up about what the correct flow should be here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the @Expensify/design team decides it should be an upss page, I’ll keep it. If not, I’ll add navigation wherever they think it’s appropriate in that situation.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure exactly where I would expect to land when I went back to the inbox, but it wouldn't be a not found screen. @dubielzyk-expensify @Expensify/product-pr do y'all have any opinions on where a user should land here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From a technical point of view, getting information about whether or not an expense is deleted is the first step to both navigating and showing information that an expense has been deleted. So I can do what you see fit.

Copy link
Contributor

Choose a reason for hiding this comment

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

My take would be that and basically just swap the text and illustration to this:

CleanShot 2025-12-08 at 13 52 38@2x

But curious to hear if @dannymcclain agrees. And @Expensify/product-pr 's take too

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that text and illustration is much better than a generic one when we know the absolute reason. The cloud guy is in a ton of places, and it's somewhat not as helpful when it's ambiguously positioned (i.e "it could be this, or that, maybe even this!").

That said, I generally don't really like the pattern of returning to an error screen if it can be avoided by removing the deleted report from the stack. So I think my preference would be something that accommodates that, really.

CC: @JmillsExpensify @mountiny

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm into that empty state because like Tom mentioned it's much more clear. But I definitely see his point about us purposely landing someone on an "error" page. I'm just having a hard time thinking of a sensible place to land someone.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I currently have a higher-priority task related to SAML. As soon as I’m done with it (most likely on Tuesday), I’ll take care of the fixes for the new empty state

const isFocused = useIsFocused();
const prevIsFocused = usePrevious(isFocused);
const firstRenderRef = useRef(true);
const [firstRender, setFirstRender] = useState(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Though this ref was introduced 2 years ago, last usage was 10 months ago.
We need to make sure that this removal doesn't reintroduce #55251

@chatgpt-codex-connector
Copy link

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to set up container
ℹ️ 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".

Comment on lines +41 to +42
// eslint-disable-next-line react-hooks/set-state-in-effect
setWasDeleted(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add comment why setting state here is safe.

Comment on lines +57 to +58
// eslint-disable-next-line react-hooks/set-state-in-effect
setWasDeleted(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here

setWasDeleted(true);
}, [currentReportID, isOptimisticDelete, userLeavingStatus]);

return useMemo(() => ({wasDeleted, parentReportID}), [wasDeleted, parentReportID]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need useMemo here? React compiler doesn't already memoize it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, we use this in the report screen, which in the current version of the code is not compiled, so useMemo is justified here.

Copy link
Contributor

@situchan situchan left a comment

Choose a reason for hiding this comment

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

Done regression test.

Confirmed these issues are not reintroduced.

@melvin-bot melvin-bot bot requested a review from luacmartins January 22, 2026 16:34
@situchan
Copy link
Contributor

@dariusz-biela please merge main. 2k+ commits behind

^

@luacmartins
Copy link
Contributor

@codex review

* Also stores the parentReportID from when the report was accessible, so it can be used for navigation
* after the report is deleted.
*/
function useReportWasDeleted(
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this hook? It seems like we use it to redirect the user to the parent report/Concierge when a report is deleted, but based on this comment, we should just show the Not found page.

Copy link
Contributor

Choose a reason for hiding this comment

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

Here's context #76012 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, thanks for the context.

@chatgpt-codex-connector
Copy link

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

We were unable to download your code in a timely manner.
ℹ️ 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".

@luacmartins
Copy link
Contributor

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to set up container
ℹ️ 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".

@mountiny
Copy link
Contributor

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to set up container
ℹ️ 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".

@situchan
Copy link
Contributor

Codex isn't willing to review this PR 😅

@mountiny mountiny marked this pull request as draft January 23, 2026 14:16
@mountiny mountiny marked this pull request as ready for review January 23, 2026 14:16
@sumo-slonik
Copy link
Contributor Author

It seems to me that it’s ready

Copy link
Contributor

@luacmartins luacmartins left a comment

Choose a reason for hiding this comment

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

LGTM

@luacmartins luacmartins merged commit 023d674 into Expensify:main Jan 23, 2026
30 checks passed
@OSBotify
Copy link
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
Contributor

🚀 Deployed to staging by https://github.com/luacmartins in version: 9.3.8-0 🚀

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

@OSBotify
Copy link
Contributor

🚀 Deployed to production by https://github.com/roryabraham in version: 9.3.8-3 🚀

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

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.

10 participants