Shopify React Native Performance

repository·main·Indexed 21 days ago

https://github.com/shopify/react-native-performance

A suite of tools for performance measurement in React Native applications. It includes the core @shopify/react-native-performance library for measuring render times, specialized profilers for lists (FlatList, FlashList) and navigation components, and a Flipper plugin for data visualization. The suite provides specialized packages for React Navigation, Apollo, and Async Storage to minimize dependencies.

Tokens
26.7K
Snippets
79
Records
113
Agent score
76%

What's inside react-native-performance

  1. Overview of React Native Performance packages

    main

    React Native Performance is a suite of profiling tools developed by Shopify to measure and analyze React Native application performance. The ecosystem is organized into a core library and several specialized extension libraries designed for specific navigation and list components.

    Core Library

    • react-native-performance: The foundational library used to measure render times across various application flows.

    Extension Libraries

    • react-native-performance-navigation: Provides higher-order profiles and components specifically for apps using React Navigation.
      • react-native-performance-navigation-bottom-tabs: Adds helper methods for @react-navigation/bottom-tabs.
      • react-native-performance-navigation-drawer: Adds helper methods for @react-navigation/drawer.
    • react-native-performance-lists-profiler: Contains utilities specifically for profiling FlatList and FlashList.
    • flipper-plugin-react-native-performance: A Flipper plugin that visualizes list profiling metrics such as Time to Interactive (TTI), blank areas, and their averages.
  2. Why `<PerformanceMeasureView>` must be used instead of manual timer calls

    main

    You cannot end a profiler timer by simply calling a function inside a screen component.

    The Problem with manual calls: Calling a function to end the timer inside a JS component only marks the moment the JS component renders. This does not represent the moment the screen is actually materialized and visible via native views.

    The Solution: <PerformanceMeasureView> works by injecting an invisible custom view as a sibling to the screen's content. The library waits until this injected view is rendered natively, providing a much more accurate approximation of when the screen actually appeared on the device.

  3. How to achieve high accuracy when starting the profiler

    main

    To get the most accurate performance reports, you should notify the profiling library of a navigation request as close to the native touch event as possible.

    The Recommended Method: When using the useStartProfiler hook, pass the GestureResponderEvent object (the first argument of a standard Touchable.onPress callback) to the profiler. This object contains nativeEvent.timestamp, which allows the library to calculate timing based on the actual native touch rather than the delayed JS execution.

    Benefits of passing GestureResponderEvent:

    1. Accuracy: It effectively uses native-layer timing, bypassing the latency of the React Native bridge and the JS event queue.
    2. Bridge Insights: The library can compute timeToConsumeTouchEvent in the RenderPassReports, helping you debug how busy the native-to-JS communication channel is.

    Why not use React Navigation events? Observing react-navigation events (like onStateChange) introduces significant inaccuracies because there is measurable latency between calling navigation.navigate() and the event being emitted. Furthermore, these events do not carry the GestureResponderEvent metadata required to reconstruct the native touch timestamp.

    Pro-tip: Use the useProfiledNavigation hook from the @shopify/react-native-performance-navigation package to simplify this process.

    // Example of high-accuracy profiling trigger
    const onPress = (event: GestureResponderEvent) => {
      // 1. Notify the profiler with the event to capture native timestamp
      startProfiler(event);
      
      // 2. Perform the actual navigation
      navigation.navigate('TargetScreen');
    };
  4. Understand the render pass model and state machine

    main

    The library models a screen's render pipeline using a state machine that tracks the lifecycle of a screen's appearance. A key concept is the render pass: a screen may undergo multiple incremental render passes (e.g., showing a loading indicator, then a partial screen, then the full screen).

    Render passes are categorized into two types:

    • Interactive: The user can interact with the screen (e.g., after data is loaded from cache or network).
    • Non-interactive: The user cannot yet interact with the screen (e.g., a loading state or a partially rendered screen).

    The library can cycle through an indefinite number of these passes, producing a RenderPassReport upon the completion of each.

  5. Use Render Watchdog Timers to catch missing reports

    main

    Render Watchdog Timers help detect developer errors where RenderPassReports are not generated. This typically happens if:

    1. A timer is started (via useStartProfiler, useResetFlow, or onAppStarted) but the screen is not wrapped with a PerformanceMeasureView.
    2. The interactive prop on PerformanceMeasureView never transitions to true.

    When enabled, the library throws a RenderTimeoutError if a screen's render timer is instantiated but the screen fails to reach the interactive state within the configured timeout duration. It is recommended to use this in development builds.

  6. Understand error types in react-native-performance

    main

    The library distinguishes between two types of errors to help you prioritize fixes:

    1. fatal errors: These indicate incorrect library usage. They are passed to the errorHandler prop in PerformanceProfiler and are thrown to your application. These are actionable and should be addressed by the developer.
    2. bug errors: These are internal issues or unhandled flows within the library. They are handled by the library itself and are only visible when running in DEBUG mode. If you encounter a bug error, you should report it as an issue on GitHub.

    Only fatal errors will reach your error monitoring dashboards.

  7. Handle RenderPassReport via PerformanceProfiler

    main

    The profiler library measures render times for profiled screens and emits a RenderPassReport object every time a profiled screen is rendered. To receive these reports, you must provide an onReportPrepared callback to the PerformanceProfiler component.

    This callback is a centralized listener: it is invoked for every profiled screen in your app. You can use this callback to console.log reports, display them in a custom UI dev tool, or send them to a telemetry service.

    If you only want to subscribe to reports for a specific screen, use the useRenderPassReport hook instead, which allows you to filter reports using a screen name regex.

    // Example of providing the callback to PerformanceProfiler
    <PerformanceProfiler onReportPrepared={(report) => {
      console.log('New render pass report:', report);
    }}>
      {/* Your app content */}
    </PerformanceProfiler>
  8. Install @shopify/react-native-performance

    main

    To install the performance measurement library in your React Native project, add the package using your preferred package manager and then install the native dependencies for iOS.

    Note: This package is no longer maintained and is considered deprecated. Consider using more modern open source alternatives for performance measurement.

    yarn add @shopify/react-native-performance
    cd ios && pod install
  9. Measure App Startup Render Time

    main

    To measure how long it takes for your app to become interactive on startup, wrap the JSX of your landing screens (e.g., Home, Login, or Welcome screens) with the PerformanceMeasureView component.

    Key Concepts:

    • Automatic Start: The timer starts automatically during the initial native app startup (after following the library's initialization steps).
    • Automatic Stop: The timer stops when the first PerformanceMeasureView is rendered and its native UI view is fully rendered. The output is a RenderPassReport.
    • Handling Render Passes: Most screens have multiple render passes (e.g., a loading state followed by data). Use the interactive prop to tell the library when the screen is actually usable by the user.

    Implementation:

    Wrap your screen component's return value. Use the interactive prop to signal when the 'final' render has occurred.

    const HomeScreen = () => {
        const {data} = useQuery(...)
        const homeItems = data?.homeItems
    
        return (
            <PerformanceMeasureView interactive={homeItems !== undefined} screenName="HomeScreen">
                {
                  homeItems === undefined
                    ? <LoadingIndicator /> : <HomeListView items={homeItems}/>
                }
            </PerformanceMeasureView>
        )
    }
  10. Measure Screen Re-render Times

    main

    To measure the time it takes for a screen to re-paint due to a UI event (like pull-to-refresh) without navigating away, use the useResetFlow hook.

    Requirements:

    1. useResetFlow: Call resetFlow({ destination: 'ScreenName' }) when the event occurs. This restarts the timer on the same screen.
    2. componentInstanceId: You must pass the componentInstanceId returned by useResetFlow into the PerformanceMeasureView component so the library can link the restart to the correct view.

    Implementation:

    const HomeScreen = () => {
      const {resetFlow, componentInstanceId} = useResetFlow();
      const {data, refetch, networkStatus} = useQuery(...)
      const homeItems = data?.homeItems
    
      const renderStateProps = {
        interactive: data !== undefined,
        renderPassName: data === undefined 
          ? 'loading' 
          : (isNetworkRequestInFlight(networkStatus) ? 'cached_render' : 'network_render')
      }
    
      return (
        <PerformanceMeasureView 
          componentInstanceId={componentInstanceId} 
          screenName="HomeScreen" 
          {...renderStateProps}
        >
          <FlatList
            onRefresh={() => {
              resetFlow({ destination: 'HomeScreen' })
              refetch()
            }}
          />
        </PerformanceMeasureView>
      )
    }
    const HomeScreen = () => {
      const {resetFlow, componentInstanceId} = useResetFlow();
    
      const {data, refetch, networkStatus} = useQuery(...)
      const homeItems = data?.homeItems
    
      const renderStateProps: RenderStateProps = {
        interactive: data !== undefined,
        renderPassName: data === undefined ? 'loading' : (isNetworkRequestInFlight(networkStatus) ? 'cached_render' : 'network_render')
      }
    
      return (
        <PerformanceMeasureView componentInstanceId={componentInstanceId} screenName="HomeScreen" {...renderStateProps}>
          <FlatList
            { /* configure your FlatList */ }
            onRefresh={() => {
              resetFlow({
                destination: 'ScreenA'
              })
              refetch()
            }}
          />
        </PerformanceMeasureView>
      )
    }