Skip to content

Add react-native-nitro-fetch #90023

@roryabraham

Description

@roryabraham

🛜 PROPOSAL: Add react-native-nitro-fetch to the app for faster network requests and prefetching on app startup 🛜

Background

App startup is one of the vital metrics Expensify tracks to evaluate app quality. The current target is P90 <=5000ms. i.e: 90% of the time, the app loads in 5000ms or less. While our P95 overall is actually ~4880ms, much of that is driven by ~1500ms P90 on the web platform. On iOS, our P90 is floating around 5500-6000ms, and on Android it's in the 3000-4000ms range.

Native app startup depends on the API layer that sends OpenApp or ReconnectApp through the shared request pipeline. These commands hydrate the initial Onyx data needed after launch, and the request path currently uses the native fetch implementation from the Hermes JS engine on mobile.

Furthermore, the existing app startup metric captures the time from when the app is launched to when the splash screen is hidden - not necessarily when it is fully hydrated and interactive. It's possible for the app to complete a native boot and render skeleton UI early, but the startup path for the user to see all their data and begin using the app depends on OpenApp/ReconnectApp.

Because these requests are scheduled from JS, they cannot begin until enough of the React Native runtime and app initialization has completed. That creates a blocking network dependency in the startup critical path: JS startup work and network latency happen mostly in sequence instead of overlapping.

Problem

When users open the app on iOS or Android, if it takes a long time to open, then they'll become frustrated and be less likely to use the product or tell others about it, which threatens Expensify's growth and revenue.

Solution

Replace native fetch with react-native-nitro-fetch, then prefetch the startup OpenApp / ReconnectApp request so it can begin before React Native JS is ready.

NitroFetch is a native-backed Fetch API implementation built on Nitro Modules. By globally replacing fetch, Headers, Request, and Response on native, existing fetch(...) callsites and dependencies keep the same API while routing through NitroFetch. Web keeps the browser fetch implementation.

For startup, prefetchOnAppStart(...) stores the OpenApp / ReconnectApp request with a stable prefetchKey. On the next cold start, native code replays that request before the React Native runtime is loaded. When JS later sends the same request with the same prefetchKey, NitroFetch can serve the prefetched response from its native cache if it is ready, or fall back to the normal network request if it is not.

Implementation

  • Add react-native-nitro-fetch as the native fetch implementation.
  • Import the native fetch polyfill at the earliest app entry point so native fetch(...) calls use NitroFetch. Web will continue to use the vanilla fetch implementation.
  • Add a prefetchKey to the startup OpenApp / ReconnectApp request path and schedule it with prefetchOnAppStart(...).
  • Clear or refresh stored prefetch data on auth changes and logout so stale credentials are not replayed on a later launch.
  • Preserve existing request behavior around abort signals, credentials, response parsing, time skew calculation, offline handling, and update-required handling.

Metrics and validation

The startup benchmark measures from native app start until the last startup network request, OpenApp or ReconnectApp, finishes. Each platform was run 20 times comparing Main release builds against the PR build with NitroFetch and startup prefetching enabled. Device and OS details are TBD for the final benchmark note.

NitroFetch also improves average native network request speed by around 15-30% across request-level timings. The startup benchmark shows a larger user-visible improvement, measuring the time from native app startup up until the OpenApp/ReconnectApp query has resolved. The benchmarks have been performed on an iPhone 14 Pro and Samsung Galaxy A14 5G with the app in release mode.

Platform Metric Main release PR with prefetch Diff Diff %
iOS P50 1676 ms 1409 ms -267 ms -15.93%
iOS P75 1722.25 ms 1446.25 ms -276 ms -16.03%
iOS P90 1733.7 ms 1525.2 ms -208.5 ms -12.03%
iOS P95 1753.45 ms 1533.95 ms -219.5 ms -12.52%
iOS Avg 1687.7 ms 1422.35 ms -265.35 ms -15.72%

|Android| P50| 3223.5 ms| 2573 ms| -650.5 ms| -20.18%|
|Android| P75| 3273.75 ms| 2598 ms| -675.75 ms| -20.64%|
|Android| P90| 3434.9 ms| 2654.4 ms| -780.5 ms| -22.72%|
|Android| P95| 3805.45 ms| 2666.25 ms| -1139.2 ms| -29.94%|
|Android| Avg| 3238.95 ms| 2544.2 ms| -694.75 ms| -21.45%|

The P90 and P95 numbers are the most important startup signal here because they show the slow-start tail improving, not only the average case. On iOS, P90 improves by 208.5 ms and P95 improves by 219.5 ms. On Android, P90 improves by 780.5 ms and P95 improves by 1139.2 ms, materially reducing the slow-start experience. This benchmark can also be added to Sentry as a subspan before deploying this feature, so we can compare before/after values in production.

Metadata

Metadata

Labels

DailyKSv2ImprovementItem broken or needs improvement.ReviewingHas a PR in review

Type

No type
No fields configured for issues without a type.

Projects

Status

CRITICAL

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions