Why React Native Needed Its Own Timing Tools

When Shopify committed fully to React Native, the engineering team faced a familiar question: is the resulting app actually as fast as a native one? The cross-platform framework uses a bridge to pass calls between JavaScript and native code, and developers wanted concrete numbers proving the user experience hadn't regressed. Existing native apps at Shopify already tracked performance with Apdex, an open standard for measuring user satisfaction with response times. For mobile, the key metric is Time To Interactive (TTI)—the moment a screen is rendered and a user can interact with it.

The challenge was obtaining TTI in a React Native context. Native rendering and JavaScript execution happen on separate threads, and a JS render completion doesn't guarantee the native UI has actually appeared. To solve this, Shopify built @shopify/react-native-performance, an open-source library for measuring rendering times in React Native apps.

Three Kinds of Render Times

The library tracks three distinct scenarios:

  • App startup render time: Starts when the native app begins (Android's onCreate or iOS's didFinishLaunchingWithOptions), stops when the first screen is mounted and fully rendered.
  • Navigation render time: Starts when a Touchable is pressed on the current screen, stops when the destination screen is mounted and fully rendered.
  • Screen re-render time: Starts when a UI event occurs (like pull-to-refresh), stops when the screen is fully rendered again after any resulting updates.

Determining what counts as "rendered" is critical. A React Native render doesn't confirm the screen is interactive. The library injects an invisible marker view with a native counterpart that reports when the view has actually moved to the window on the native side. This measures true end-to-end performance, including React Native-to-native bridge communication time, which is the TTI the team wants to track.

Render Passes and the State Machine

Screens rarely become interactive in a single step. A typical flow might show a loading indicator, then render part of the screen, then render everything. The library models these incremental steps as render passes. A pass can be interactive (the user can interact afterward, e.g., content from cached or network data) or not (e.g., a loading or partially rendered screen).

Internally, a state machine tracks each screen's pipeline:

  1. Started: The flow begins—an app launch, a navigation press, or a pull-to-refresh trigger.
  2. Mounted: The JavaScript component has loaded.
  3. Rendered: Native rendering is complete, emitting a Render Pass Report.
  4. Unmounted: The screen is left. The library produces a RenderPassReport if the screen was unmounted before any interactive render pass completed—a signal that the user gave up waiting and backed out.
React Native Performance state machine diagram
A diagram of React Native Performance's state machine.

The most important field in the resulting report is time to render, which represents the wait between, say, a navigation button press and the moment the next screen is interactive. The report also records the sourceScreen and destinationScreen, plus a flowInstanceId that tracks render passes belonging to the same user flow.

Getting Started: From Native to TypeScript

Integration requires initialization in all three layers of a React Native app.

On Android, add the profiler call to your MainApplication.java. It must be the first line in onCreate—starting as early as possible ensures accurate measurements. The iOS equivalent goes in AppDelegate.m, also as the first line in the startup routine. These calls start a timer on the native side.

On the TypeScript side, mount the PerformanceProfiler component high in your app tree, then identify the screens a user can land on at startup. Wrap those screens' JSX with PerformanceMeasureView. The library automatically recognizes the first rendered PerformanceMeasureView as the landing screen, waits for its native UI view to render, and emits a RenderPassReport with the timeToBootJsMillis field—your application startup measurement.

Measuring Navigation

For navigation, the timer starts when a Touchable on the current screen is pressed. Use the useStartProfiler hook to notify the library of the flow start, then wrap the destination screen with PerformanceMeasureView as before.

If you use react-navigation, the companion @shopify/react-native-performance-navigation package provides useProfiledNavigation, which combines the two calls—profiling start and navigation request—into one, ensuring full coverage of navigation flows. It also offers ReactNavigationPerformanceView, a drop-in replacement for PerformanceMeasureView used with @react-navigation. Its API is identical, but it adds a transition-end render pass: the destination screen isn't marked interactive until the navigation transition animation completes, yielding more precise measurements that account for the animation timing reported by @react-navigation.

@shopify/react-native-performance-navigation-bottom-tabs

Additional helper methods for @react-navigation/bottom-tabs library 

@shopify/react-native-performance-navigation-drawer

Additional helper methods for @react-navigation/drawer library.

@shopify/react-native-performance-lists-profiler

Components for profiling FlatList and FlashList.

@shopify/flipper-plugin-shopify-react-native-performance

A Flipper plugin is available to make lists profiling easier. The plugin visualizes TTI, blank areas, and its averages in a convenient manner.

The core library lives in a performance monorepo alongside companion packages for related performance topics. Each report's fields are documented in the library's documentation, which details the full RenderPassReport structure.

Getting Measurements onto a Dashboard

Once the library is integrated, the next step is typically pushing those measurements into an external monitoring system. PerformanceProfiler exposes an onReportPrepared callback that fires on each new event with a JSON object. That payload can be forwarded to any preferred analytics tool.

As a minimal example, forwarding events to Amplitude after integrating its React Native SDK requires just one line inside the callback:

Dashboard settings
The dashboard settings.

With events flowing into a dashboard, you can track how rendering times evolve per screen and even break down the individual rendering steps to isolate bottlenecks. The authors provide a demo project in the react-native-performance-reporting-demo repo that streams live events to a shared Amplitude dashboard.

Interpreting the Charts

The sample dashboard applies the PERCENTILE(A, 0,99) formula to compute the 0.99 percentile TTI per app screen by day. With limited events, nearly all data points are included, but a smaller percentile becomes more appropriate as event volume grows.

The resulting 0.99 percentile TTI per app screen by day graph
The resulting 0.99 percentile TTI per app screen by day graph.

The blue peak on November 18th represents the TTI for the "Performance" screen, which deliberately includes a five-second artificial delay to illustrate the different rendering steps. The chart also makes it easy to compare screens side by side, spot obvious deviations, and watch TTI trends over time to catch regressions early.

Internally, Shopify uses a dashboard that segments by screen name and calculates an Apdex score for each screen from the reported metrics, giving teams a direct view into which screens are underperforming and whether that's improving or worsening.

Feedback and Context

Interest in React Native performance continues to grow. Performance-focused talks at React Native EU and App.js conferences this year, along with new tooling like the Reassure performance testing library and Coinbase's forthcoming Performance Vitals system, all point in the same direction. The popularity of FlashList—Shopify's performant alternative to FlatList—reinforces that developers are actively seeking faster rendering paths.

The react-native-performance library is open for experimentation, and the team welcomes feedback from developers who try it out.