Skip to content

[WIP] perf: preload the Reports tab so the first Inbox open runs the warm path - #100042

Draft
BartekObudzinski wants to merge 1 commit into
Expensify:mainfrom
callstack-internal:preload-inbox-tab-warmup
Draft

[WIP] perf: preload the Reports tab so the first Inbox open runs the warm path#100042
BartekObudzinski wants to merge 1 commit into
Expensify:mainfrom
callstack-internal:preload-inbox-tab-warmup

Conversation

@BartekObudzinski

@BartekObudzinski BartekObudzinski commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

The Inbox tab is registered with lazy: true, so nothing mounts ReportsSplitNavigator until the user taps Inbox for the first time. That first tap then pays for the lazy chunk, the sidebar and, on wide layouts, the central ReportScreen, all inside the ManualNavigateToInboxTab span.

This adds InboxTabWarmup, rendered by the tab bar, which calls navigation.preload(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) in the first idle window after OpenApp is applied. React Navigation renders a preloaded tab as if lazy were false, so the first tap now runs the same path as a re-visit to an already mounted tab. The dispatch happens from the tab bar because that is where the tab navigator's own navigation object lives, and its router is the one that handles the preload action.

Two guards keep the preload from doing harm. It skips when Reports is the focused tab, and it skips when the route already has nested state. In both cases the router would still add the key to preloadedRouteKeys, and BottomTabView drops shouldFreeze for preloaded routes, so the scene would stay unfrozen in the background despite freezeOnBlur: true. The session flag lives in inboxTabPreloadState.ts and resets when the warm-up unmounts, because signing out unmounts the tab navigator without tearing down the JS runtime.

A new is_preloaded span attribute records whether the session preloaded the tab, so the effect stays visible on the dashboards. It describes the session rather than the single navigation, so pair it with is_warm to isolate the first open.

Measured on web in a wide layout, five cold starts each, all with is_warm: false. Staging serves current main, the adhoc build serves this branch, both minified, same machine and account:

median min-max
staging (main) 405 400-915
adhoc (this PR) 182 177-199

That is -223ms on the median, or -55%. The spread matters as much as the median: staging ranges over 515ms because the first tap fetches the lazy chunk over the network, and the adhoc build ranges over 22ms because that fetch has already happened. Staging spans also carry lazy_tab_fallback_shown: true, which the preloaded ones do not.

The same comparison on a local dev bundle gave 656 -> 429 median. The dev win looks smaller in relative terms because an unminified bundle inflates both sides.

This does not make a cold open as cheap as a warm one. On web an inactive tab gets display: none, so the preloaded scene never lays out and the LHN list renders no rows. Layout, the first row render and paint still land on the tap.

Fixed Issues

$
PROPOSAL:

Tests

  1. Sign in on web in a wide layout, landing on Home or Search rather than Inbox.
  2. Open the console and wait a few seconds for the app to settle.
  3. Click the Inbox tab. Verify the report list appears without the tab-level loading spinner that used to show while the lazy chunk resolved.
  4. Verify the ManualNavigateToInboxTab span logged in the console carries is_preloaded: true.
  5. Switch to Search and back to Inbox several times. Verify navigation still works and the report list keeps its scroll position.
  6. Open a deep link straight to a report (/r/<reportID>) so the app launches with Inbox focused. Verify the report opens normally, then switch to Search and back and verify nothing is broken.
  7. Sign out and sign in as a different account. Verify the Inbox tab still opens correctly on the first click.
  8. Repeat steps 1 to 3 in a narrow layout.
  • Verify that no errors appear in the JS console

Offline tests

The warm-up waits for isLoadingApp to be false, which a relaunch reads from disk, so it runs offline as well and builds the sidebar from the persisted collections.

  1. Sign in, then go offline.
  2. Reload the app and wait for it to settle on Home.
  3. Click the Inbox tab. Verify the cached report list renders and the offline indicator behaves as it does today.
  4. Go back online and verify the list updates normally.

QA Steps

Same as tests.

  • 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 followed proper code patterns (see Reviewing the code)
    • 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 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)
  • 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)
  • 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.
  • 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
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

@BartekObudzinski BartekObudzinski changed the title perf: preload the Reports tab so the first Inbox open runs the warm path [WIP] perf: preload the Reports tab so the first Inbox open runs the warm path Sep 1, 2026
@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Coverage Δ
src/CONST/index.ts 91.07% <ø> (ø)
...nts/Navigation/NavigationTabBar/InboxTabButton.tsx 93.18% <ø> (ø)
...avigation/AppNavigator/Navigators/TabNavigator.tsx 0.00% <0.00%> (ø)
src/libs/Navigation/inboxTabPreloadState.ts 42.85% <42.85%> (ø)
...igation/AppNavigator/Navigators/InboxTabWarmup.tsx 0.00% <0.00%> (ø)
... and 29 files with indirect coverage changes

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run 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.

1 participant