Skip to content

[Performance] Pre-render Search under RHP for instant post-submit navigation on mobile#87404

Draft
JakubKorytko wants to merge 5 commits intoExpensify:mainfrom
software-mansion-labs:korytko/perf/pre-insert-search-under-rhp
Draft

[Performance] Pre-render Search under RHP for instant post-submit navigation on mobile#87404
JakubKorytko wants to merge 5 commits intoExpensify:mainfrom
software-mansion-labs:korytko/perf/pre-insert-search-under-rhp

Conversation

@JakubKorytko
Copy link
Copy Markdown
Contributor

@JakubKorytko JakubKorytko commented Apr 8, 2026

Explanation of Change

On narrow layout (mobile), pre-renders the Search page behind the RHP while the user is on the expense confirmation screen, so post-submit navigation is instant.

How it works

Phase What happens
Mount (confirmation screen) After a 300ms delay, the Search fullscreen route is inserted underneath the RHP via REPLACE_FULLSCREEN_UNDER_RHP. The Search page mounts in the background while the user fills in details.
Submit (fast path) If the pre-insert fired, the RHP is dismissed instantly to reveal the already-mounted Search. createTransaction runs in the next frame - "dismiss first, compute later".
Submit (slow path) If the user taps submit before the 300ms timer fires, the existing non-pre-insert navigation path is used as a fallback.
Back out (cleanup) If the user leaves without submitting, the pre-inserted route is removed via the new REMOVE_FULLSCREEN_UNDER_RHP action.

Supporting changes

  • reserveDeferredWriteChannel - pre-creates the deferred write channel so Search always sees hasDeferredWrite=true on mount
  • SearchPageNarrow - gates phase transitions on focus (useFocusEffect) to avoid wasted work while hidden behind the RHP
  • SearchStaticList - shows the pending expense placeholder on focus when a submit action is pending
  • RightModalNavigator - disables slide-out animation when a pre-insert is active so the dismiss reveals the destination instantly

Performance gains (ManualSubmitToDestinationVisible (navigate_to_search) span):

Platform main avg (ms) This branch avg (ms) Improvement
Android 615 197 -418 ms (68%)
iOS 1118 222 -896 ms (80.1%)

Fixed Issues

$ #83634
PROPOSAL: N/A

Tests

  1. Fast-path submit (pre-insert fires before user taps submit)

    1. Open the app on a narrow-layout device (mobile or narrow browser window) and do not navigate to inbox/search
    2. Tap the FAB (+) button and start creating a new expense (e.g. Manual -> enter amount -> select participant)
    3. On the confirmation screen, wait at least 300ms (the pre-insert delay)
    4. Tap Submit / Create
    5. Verify that the Search page appears instantly without a visible navigation transition - the RHP dismisses to reveal the already-mounted Search screen with a pending expense placeholder row
  2. Slow-path submit (user taps submit before 300ms pre-insert fires)

    1. Open the app on a narrow-layout device and do not navigate to inbox/search
    2. Tap the FAB (+) and quickly go through the expense creation flow
    3. On the confirmation screen, tap Submit immediately (within ~300ms of the screen appearing)
    4. Verify that the app still navigates to the Search page correctly (via the existing non-pre-insert path) and no errors appear
  3. Back-out cleanup (user leaves confirmation without submitting)

    1. Open the app on a narrow-layout device and do not navigate to inbox/search
    2. Start creating an expense and reach the confirmation screen
    3. Wait at least 300ms for the pre-insert to fire
    4. Press the back button or swipe to go back without submitting
    5. Verify that the pre-inserted Search route is removed - you should return to the previous screen (not the Search page), and no stale Search route remains in the navigation stack

Offline tests

  1. Enable airplane mode / disable network on the device
  2. Tap the FAB (+) and create a new expense through the confirmation screen while being outside of inbox/search
  3. Wait 300ms, then tap Submit
  4. Verify the Search page appears with the optimistic pending expense placeholder row
  5. Re-enable network and verify the expense syncs and the placeholder is replaced with the confirmed row

QA Steps

Same as tests

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 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.mov
Android: mWeb Chrome
iOS: Native
iOS.mov
iOS: mWeb Safari
MacOS: Chrome / Safari

@JakubKorytko
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45ad0dfae9

ℹ️ 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".

@JakubKorytko
Copy link
Copy Markdown
Contributor Author

@codex review

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 8, 2026

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 Δ
src/CONST/index.ts 93.65% <ø> (ø)
.../routerExtensions/addRootHistoryRouterExtension.ts 100.00% <100.00%> (ø)
src/pages/Search/SearchPageNarrow/index.tsx 81.48% <100.00%> (+0.34%) ⬆️
...igator/createRootStackNavigator/RootStackRouter.ts 73.84% <75.00%> (+1.71%) ⬆️
src/components/Search/SearchStaticList.tsx 4.00% <0.00%> (-0.26%) ⬇️
...on/AppNavigator/Navigators/RightModalNavigator.tsx 5.15% <0.00%> (-0.17%) ⬇️
src/libs/telemetry/submitFollowUpAction.ts 23.40% <0.00%> (-1.05%) ⬇️
src/libs/actions/IOU/index.ts 78.33% <50.00%> (-0.15%) ⬇️
src/libs/deferredLayoutWrite.ts 81.25% <30.76%> (-18.75%) ⬇️
src/components/Search/index.tsx 0.93% <0.00%> (-0.02%) ⬇️
... and 3 more
... and 39 files with indirect coverage changes

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 10c7e8925d

ℹ️ 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".

@JakubKorytko
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9cbc1caae8

ℹ️ 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".

@JakubKorytko
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9cbc1caae8

ℹ️ 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".

@JakubKorytko
Copy link
Copy Markdown
Contributor Author

@codex review 🙏

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47a7bcce47

ℹ️ 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".

@JakubKorytko
Copy link
Copy Markdown
Contributor Author

@codex review

(do not repeat yourself 🙏)

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

pendingSubmitFollowUpAction = {followUpAction, reportID};
span.setAttribute(CONST.TELEMETRY.ATTRIBUTE_SUBMIT_FOLLOW_UP_ACTION, followUpAction);

P1 Badge Keep follow-up action as navigate_to_search for fast path

When this same-flow branch runs, it still overwrites pendingSubmitFollowUpAction (and the span attribute) with the new value. In the fast pre-insert flow, onConfirm sets NAVIGATE_TO_SEARCH, then handleNavigateAfterExpenseCreate() can later call DISMISS_MODAL_ONLY after Search is already on top; this code reclassifies that submit as dismiss-only, which skews submit-to-destination telemetry for the very path this PR is measuring. The NAVIGATE_TO_SEARCH -> DISMISS_MODAL_ONLY refinement should preserve the original action rather than replacing it.

ℹ️ 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".

// isFromGlobalCreate and iouType are stable for the lifetime of this screen instance
// since they derive from route params / Onyx and don't change while the confirmation screen is open.
// eslint-disable-next-line react-hooks/exhaustive-deps -- Pre-insertion is a one-time side effect on mount.
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Re-run preinsert setup after transaction finishes loading

This pre-insert setup effect is one-shot ([]) but immediately bails when transaction is not yet available. IOURequestStepConfirmation can render before Onyx transaction data resolves, so in those cases the timer is never scheduled later and the new fast-path optimization silently never activates. The effect should depend on transaction/loading state (or explicitly trigger when loading completes) so late-loaded transactions still get pre-insert behavior.

Useful? React with 👍 / 👎.

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