Performance problems in mobile apps do not announce themselves — they accumulate silently until users feel the app is “slow” and leave a 2-star review. The challenge is that React Native apps have two execution environments (JavaScript thread and native UI thread), and a bottleneck in either one produces the same visible symptom: dropped frames and sluggish interactions.Profiling is detective work. You need different tools for different suspects: the React Profiler for re-render issues, native profilers (Xcode Instruments, Android Profiler) for native-side bottlenecks, and custom metrics for production monitoring. This module covers them all.What You’ll Learn:
The React Profiler answers the question: “Which components are rendering, how often, and how long does each render take?” This is your first stop when investigating JavaScript-thread performance issues. The key insight: a component re-rendering is not inherently bad — re-rendering unnecessarily (with the same props and state) is where performance is wasted.
// Enable profiling in development// The Profiler is available in React DevTools// Wrap components to profileimport { Profiler, ProfilerOnRenderCallback } from 'react';const onRenderCallback: ProfilerOnRenderCallback = ( id, phase, actualDuration, baseDuration, startTime, commitTime) => { console.log({ id, // Component name phase, // "mount" or "update" actualDuration, // Time spent rendering baseDuration, // Estimated time without memoization startTime, // When React started rendering commitTime, // When React committed the update });};function App() { return ( <Profiler id="App" onRender={onRenderCallback}> <MainContent /> </Profiler> );}
Built-in tools show you what is happening during development, but production is where performance actually matters. Users on 3-year-old Android phones with spotty cellular connections experience your app very differently from your development MacBook. Custom metrics bridge this gap by collecting timing data from real user sessions.The pattern below is a lightweight performance tracking service that measures arbitrary operations, buffers the results, and flushes them to your backend after interactions complete (so the metrics collection itself does not cause jank).
Application Performance Monitoring (APM) tools give you a production-grade observability layer: automatic transaction tracing, error correlation, and dashboards that show how your app performs across device types, OS versions, and network conditions. If custom metrics are a thermometer, APM is a full medical workup.For most React Native teams, Sentry is the best starting point — it handles both crash reporting and performance monitoring in a single SDK, with excellent React Native support.
Frame rate is the single most important performance metric for mobile apps. Anything below 60 FPS (16.67ms per frame) is perceptible as jank. Drops below 30 FPS feel genuinely broken. The hook below measures frame timing using requestAnimationFrame and classifies frames as “jank” when they exceed the 16.67ms budget.This is a development tool — do not ship it in production builds, as the measurement itself consumes resources. Use APM tools for production frame rate tracking.
Each tool in the profiling stack answers a different question. Using the wrong tool wastes time — you will see misleading data or miss the actual bottleneck entirely.
Production observability; understanding P50/P95 latencies across devices
Cost at scale; sampling means you miss some events
Decision framework — where to start:
“The app feels slow” — Start with React DevTools Profiler. Most perceived slowness in React Native comes from unnecessary re-renders on the JS thread.
“Animations are janky” — If the JS thread profiler shows renders completing under 16ms but animations still drop frames, the bottleneck is on the native thread. Switch to Xcode Instruments or Android Profiler.
“Users report slowness but I can’t reproduce it” — You need production metrics. Set up APM (Sentry) and custom performance tracking to see what real users experience on real devices.
“Startup takes too long” — Instrument startup milestones (JS load, first render, interactive). Compare against budgets: cold start under 2 seconds is the target; under 1 second is excellent.
“The app crashes after extended use” — This is likely a memory leak. Use the Android Profiler’s memory timeline or Xcode’s Memory Graph to find retained objects.
If your app runs on Hermes (the default since React Native 0.70 and Expo SDK 48+), profiling behavior differs from JavaScriptCore (JSC) in important ways:
Aspect
Hermes
JSC
Bytecode
Precompiled at build time — faster startup
JIT compiled at runtime — slower startup, potentially faster peak
Heap snapshots
Available via HermesInternal.getRuntimeProperties()
Not directly accessible from JS
Sampling profiler
Built-in: HermesInternal.enableSamplingProfiler()
Requires external tools
Chrome DevTools
Direct connection via hermes inspector
Via jsc inspector
Memory overhead
Lower baseline (~3-5 MB typical)
Higher baseline (~8-15 MB typical)
Profiling accuracy
Accurate even in dev mode (no JIT warm-up artifacts)
JIT optimization can make dev profiling misleading
To take a Hermes CPU profile programmatically:
if (global.HermesInternal) { // Start sampling profiler global.HermesInternal.enableSamplingProfiler(); // ... run the code you want to profile ... // Stop and save the profile global.HermesInternal.disableSamplingProfiler(); global.HermesInternal.dumpSamplingProfiler('/tmp/hermes-profile.cpuprofile'); // Open in Chrome DevTools: chrome://tracing}
Defining concrete performance budgets turns vague “make it faster” requests into measurable, actionable targets. These budgets should be enforced in CI — not just aspirational numbers in a wiki.
Development mode enables React’s reconciler warnings, Strict Mode double-renders, and disables most optimizations. Profiling in dev mode will show every component rendering 2x and taking 3-5x longer than production. Always profile release builds. For Expo:
# Build a production-like profile for profilingnpx expo run:ios --configuration Releasenpx expo run:android --variant release
The act of profiling changes what you measure. Flipper adds network overhead. The React Profiler’s onRender callback adds JS thread work. The FPS monitor hook from this module uses requestAnimationFrame (which itself consumes frame budget). Profile with one tool at a time, and remove profiling instrumentation from production builds.
In the classic React Native architecture (pre-New Architecture), every communication between JS and native crosses the bridge asynchronously. This means a “slow render” might actually be a slow bridge crossing — the JS code finished fast, but the native update was queued behind other bridge traffic. With the New Architecture (Fabric + TurboModules), this is less of an issue because communication is synchronous. Check whether your app uses the New Architecture before spending time optimizing bridge traffic.
JS heap growing during a session does not always mean a leak. V8 and Hermes both use generational garbage collectors that may delay collection. A true leak is memory that grows monotonically and never decreases even after GC runs. To confirm: force GC (in Chrome DevTools attached to Hermes, click the trash can icon), then check if memory drops. If it does, you had garbage, not a leak.
Collecting metrics from every user session overwhelms your backend and inflates costs. Use tiered sampling:
Metric Type
Sample Rate
Rationale
Crash reports
100%
Every crash matters — never sample these
Startup timing
20-50%
Enough to detect regressions; high volume data
Screen load times
10-20%
Statistical significance at scale; does not need every user
Custom business metrics
5-10%
Supplement with server-side data for full picture
Detailed transaction traces
1-5%
Expensive to store; use for deep-dive debugging
Adjust these rates upward for smaller user bases. If you have under 10,000 DAU, sample at 100% for everything — the volume is manageable and you cannot afford to miss patterns.