Metrics reference

Edit page

A reference of each performance metric tracked by Expo Observe, including concepts and data handling.


For the complete documentation index, see llms.txt. Use this file to discover all available pages.

Reference for the performance metrics Expo Observe collects, the core concepts used to organize events (sessions and users), and how collected data is retained.

Concepts

Session

A session starts when the app process is launched and ends when the app process is terminated. Each session has a unique identifier and contains all metrics collected during that app launch.

User

A user is identified by an anonymous ID that is unique per app installation. This ID:

  • Is generated when the app is first installed
  • Persists across app updates
  • Is reset if the user uninstalls and reinstalls the app
  • Is not Personally Identifiable Information (PII)

This allows you to see metrics across multiple sessions for the same user without collecting personal data.

Metrics

All duration metrics are reported in seconds.

Cold launch time

What it measures: Time from process creation to when the system has finished allocating memory, starting a fresh runtime environment, loading the app's code and resources from disk, and initializing its components before rendering the UI. This is the slowest type of launch and typically occurs after a fresh install, app upgrade, device reboot, or when the OS has killed the app to reclaim memory. It is a native-only metric, meaning your JavaScript code does not affect this metric, but the React Native runtime initialization is included.

This metric is collected automatically.

How to improve:

  • Remove unused native modules.
  • Avoid static initializers (+load methods in Objective-C, static constructors in C++), and native modules or config plugins that add them.
  • Keep the app's memory and CPU usage low so the OS does not kill the process while in the background. This does not affect this metric itself, but makes subsequent launches warm instead of cold.
  • If you use expo-updates with a non-zero fallbackToCacheTimeout, the app launch is blocked waiting for an update check. Keep this value at 0 (default), or set checkOnLaunch to NEVER or ERROR_RECOVERY_ONLY to avoid delaying cold launches.

Our recommendation: under 1.5s.

Warm launch time

What it measures: A warm launch happens when the OS already has the app process in memory and only needs to bring it back to the foreground and recreate the view hierarchy. Unlike a cold launch, most native resources and services are already in memory, so this type of launch is significantly faster.

Apps can't pre-warm themselves: the OS decides which processes stay in memory based on system pressure and recent use. You can influence the duration of a warm launch, but not whether one happens.

This metric is collected automatically.

How to improve:

  • Remove unused native modules.
  • Reduce the number of views in the view hierarchy. The OS has to recreate the view tree on warm launch, so a deeply nested or bloated tree takes longer to restore.

Our recommendation: under 0.5s.

Bundle load time

What it measures: Duration of loading the JavaScript bytecode and evaluating it. This starts when the bundle begins loading and ends when the bundle finishes evaluating, before runApplication is called.

This metric is collected automatically.

How to improve:

  • Reduce the bundle size:
  • Lazy-load large screens and components with React.lazy(). See Optimize JavaScript loading.
  • Avoid blocking the JavaScript thread in the top-level scope:
    • Don't do heavy computations.
    • Defer any synchronous I/O operations (storage reads and writes).

Our recommendation: under 0.3s.

Time to first render (TTR)

What it measures: Time from when the app finishes native launching to when the root React component first renders on the screen. This is the moment actual content is rendered by React, after the splash screen is hidden. The goal for every app should be to show something meaningful as fast as possible, even if it's a skeleton loading screen.

This metric is collected automatically when you wrap your root layout with AppMetricsRoot (see Get started). You can also call AppMetrics.markFirstRender() manually if needed.

How to improve:

  • Reduce bundle load time (see above).
  • Avoid synchronous I/O operations (storage reads and writes).
  • Avoid blocking on network requests.
  • Keep the initial render tree small (defer heavy components).
  • Use a lightweight screen as the initial route.
  • Minimize useEffect and useLayoutEffect chains that block rendering.

Our recommendation: under 2s including the cold launch time.

Time to interactive (TTI)

What it measures: Time between the warm/cold launch and when the user can actually tap, scroll, and interact with the app in other ways. It is the most important startup metric because it is what users perceive as "the app is ready".

This metric is not reported automatically. To start measuring it, call AppMetrics.markInteractive() once the screen is ready for user interaction, for example in a useEffect that runs after your initial data has loaded. If your app uses deep links, the main screen may not always be the initial screen, so we recommend calling this function on other screens too. Only the first call per launch is recorded, so it is safe to call it multiple times (for example, when the user navigates between screens). If you use expo-router, the metric's event automatically includes the current route name.

What makes an app "interactive"?

All of these must be true:

  • Content is rendered on the screen (not just a splash or skeleton).
  • Touch handlers are attached and responsive.
  • Navigation is functional.

How to improve measurement accuracy: Call AppMetrics.markInteractive() only after the screen's content is loaded and touch handlers are active, not just on component mount. If your screen fetches data before becoming usable, place the call after the data is ready.

How to improve:

  • Reduce time to first render (see above).
  • Avoid waterfall data fetches before showing interactive content.
  • Optimize initial network requests.
  • Avoid rendering large lists (use FlashList or LegendList).
  • Reduce heavy work that may block the JavaScript thread and interactions (I/O operations, state hydration, JSON parsing).
  • If possible, show cached or local data first.

Our recommendation: under 3s including the cold launch time.

Extra params

Each TTI event includes extra params to help triage issues:

  • expo.frameRate.slowFrames (count): Frames that took 17ms or longer to render. If this is high relative to the startup duration, the main thread was consistently busy during launch. Points to heavy layout work, synchronous bridge calls, or too many components rendering at once.
  • expo.frameRate.frozenFrames (count): Frames that took 700ms or longer to render. These are hard freezes where the app visibly hung. Even one during startup is a serious issue. Usually caused by synchronous I/O, large JSON parsing, or blocking network calls on the main thread.
  • expo.frameRate.totalDelay (seconds): Total accumulated time all frames exceeded their target duration. This is the single best "smoothness" number. Compare it to TTI: if TTI is 2.5s and totalDelay is 0.1s, startup was slow but smooth (the time was spent on legitimate work). If totalDelay is 1.5s, the app was janky for most of the startup, and the user was staring at a stuttering screen.

How to interpret them:

  • High TTI + low total delay: startup is slow but smooth. Optimize what's blocking the launch sequence (bundle size, data fetching, initialization chains).
  • High TTI + high total delay + many slow frames: main thread contention. Offload work and simplify the initial render tree.
  • High TTI + high delay + frozen frames: something is blocking hard. Look for synchronous I/O, large JSON parsing, or blocking API calls.

Data handling

Offline collection

Metrics collected while the device is offline are stored on the device. They are automatically sent to the server when the app moves to the background, provided there is connectivity. You can also call ExpoObserve.dispatchEvents() to flush events manually at any time.

Data retention

Metric data is retained for a minimum of 60 days.

Sampling

All sessions are tracked. There is no sampling applied to the data collection.

Environment

All metrics are grouped by environment. The environment value is derived from process.env.NODE_ENV by default, or can be overridden via configure({ environment }). Metrics with the development environment are filtered out before dispatching unless enableInDebug is set to true in your app config (for information, see Enable metrics in development). You can also disable all dispatching globally using configure({ dispatchingEnabled: false }).