Skip to main content

Profiling React Native Apps

Profiling is essential for identifying performance bottlenecks in your React Native application. Learn how to use various profiling tools to analyze and optimize your app.

Performance Monitor

Enable the built-in performance monitor to see real-time metrics:
// In-app developer menu
// Shake device or press Cmd+D (iOS) / Cmd+M (Android)
// Select "Show Performance Monitor"
The monitor displays:
  • JavaScript frame rate
  • UI frame rate
  • Views count
  • RAM usage

React DevTools Profiler

Installation

npm install -g react-devtools

Using the Profiler

1

Start React DevTools

react-devtools
2

Connect your app

Start your React Native app with DevTools enabled. The app will automatically connect.
3

Record a profiling session

Click the Profiler tab, then click the record button to start profiling.
4

Analyze results

View flame charts showing component render times and why components re-rendered.

Using the Profiler API

import React, { Profiler } from 'react';

function onRenderCallback(
  id, // the "id" prop of the Profiler tree that has just committed
  phase, // either "mount" (if the tree just mounted) or "update" (if it re-rendered)
  actualDuration, // time spent rendering the committed update
  baseDuration, // estimated time to render the entire subtree without memoization
  startTime, // when React began rendering this update
  commitTime, // when React committed this update
  interactions // the Set of interactions belonging to this update
) {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
  
  if (actualDuration > 16) {
    console.warn(`${id} exceeded 16ms frame budget!`);
  }
}

const App = () => (
  <Profiler id="App" onRender={onRenderCallback}>
    <NavigationContainer>
      <MainNavigator />
    </NavigationContainer>
  </Profiler>
);

Performance Timeline API

React Native implements the Web Performance API:
import { performance } from 'react-native';

// Mark points in time
performance.mark('fetch-start');
await fetchData();
performance.mark('fetch-end');

// Measure duration between marks
performance.measure('data-fetch', 'fetch-start', 'fetch-end');

// Get measurements
const measures = performance.getEntriesByType('measure');
measures.forEach(measure => {
  console.log(`${measure.name}: ${measure.duration}ms`);
});

Performance Observer

Automatically monitor performance entries:
import { PerformanceObserver } from 'react-native';

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log(`${entry.name}: ${entry.duration}ms`);
    
    // Log slow operations
    if (entry.duration > 100) {
      console.warn('Slow operation detected:', entry);
    }
  });
});

// Observe all measure entries
observer.observe({ entryTypes: ['measure'] });

// Cleanup
observer.disconnect();

JavaScript Thread Profiling

Using Chrome DevTools

1

Enable remote debugging

Open the in-app developer menu and select “Debug”.
2

Open Chrome DevTools

Navigate to chrome://inspect in Chrome.
3

Start profiling

Go to the Performance tab and click Record.
4

Interact with app

Perform the actions you want to profile.
5

Analyze results

Stop recording and analyze the flame chart for bottlenecks.

Using Hermes Profiler

Hermes includes a built-in sampling profiler:
// Enable Hermes profiling
const Hermes = require('hermes-engine');

// Start profiling
Hermes.samplingProfiler.enable();

// Your code here
expensiveOperation();

// Stop profiling and get profile
const profile = Hermes.samplingProfiler.stop();

// Convert to Chrome format
const chromeProfile = Hermes.samplingProfiler.dumpSampledTraceToFile();
console.log('Profile saved:', chromeProfile);
Visualize the profile:
npx react-native profile-hermes [profile-file]

Native Thread Profiling

Android Profiling

Systrace

Capture system-level performance:
# Record 5-second trace
python $ANDROID_HOME/platform-tools/systrace/systrace.py \
  --time=5 \
  -o trace.html \
  sched gfx view wm am dalvik input
Open trace.html in Chrome to analyze.

Android Studio Profiler

1

Open Android Studio

Open your project in Android Studio.
2

Run profiler

Click View > Tool Windows > Profiler.
3

Select process

Click the + button and select your app process.
4

Profile CPU

Click the CPU row to start CPU profiling:
  • Sample C/C++ Functions
  • Trace System Calls
  • Sample Java Methods

Method Tracing

// Add to Android code
import android.os.Debug

Debug.startMethodTracing("MyApp")
// Code to profile
Debug.stopMethodTracing()

// Pull trace file
// adb pull /sdcard/Android/data/com.yourapp/files/MyApp.trace

iOS Profiling

Instruments

1

Build for profiling

In Xcode, select Product > Profile (Cmd+I).
2

Choose template

Select a profiling template:
  • Time Profiler: CPU usage
  • Allocations: Memory allocations
  • Leaks: Memory leaks
  • System Trace: System-level events
3

Record and analyze

Click the record button, use your app, then stop and analyze results.

Time Profiler

Analyze CPU usage:
// Add to iOS code for manual instrumentation
#import <os/signpost.h>

os_log_t log = os_log_create("com.yourapp", "Performance");
os_signpost_id_t spid = os_signpost_id_generate(log);

os_signpost_interval_begin(log, spid, "ExpensiveOperation");
// Code to profile
os_signpost_interval_end(log, spid, "ExpensiveOperation");

Component Render Profiling

Why Did You Render

Detect unnecessary re-renders:
npm install @welldone-software/why-did-you-render
// wdyr.js
import React from 'react';

if (__DEV__) {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
    trackHooks: true,
    logOwnerReasons: true,
  });
}
// index.js
import './wdyr'; // Must be first
import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('MyApp', () => App);

React.memo Profiling

import React, { memo } from 'react';

const MyComponent = memo(
  ({ data }) => {
    console.log('MyComponent rendered');
    return <View>{data}</View>;
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip render)
    const areEqual = prevProps.data === nextProps.data;
    if (!areEqual) {
      console.log('Re-rendering due to data change');
    }
    return areEqual;
  }
);

Frame Rate Analysis

Monitor Frame Drops

import { InteractionManager } from 'react-native';

class FrameMonitor {
  constructor() {
    this.frameCount = 0;
    this.lastTime = Date.now();
    this.requestAnimationFrame = this.requestAnimationFrame.bind(this);
  }
  
  start() {
    this.handle = requestAnimationFrame(this.requestAnimationFrame);
  }
  
  requestAnimationFrame() {
    this.frameCount++;
    const now = Date.now();
    const elapsed = now - this.lastTime;
    
    if (elapsed >= 1000) {
      const fps = (this.frameCount / elapsed) * 1000;
      console.log(`FPS: ${fps.toFixed(2)}`);
      
      if (fps < 55) {
        console.warn('Frame rate dropped below 55 FPS!');
      }
      
      this.frameCount = 0;
      this.lastTime = now;
    }
    
    this.handle = requestAnimationFrame(this.requestAnimationFrame);
  }
  
  stop() {
    cancelAnimationFrame(this.handle);
  }
}

const monitor = new FrameMonitor();
monitor.start();

Memory Profiling

Track Memory Usage

import { NativeModules } from 'react-native';

const getMemoryInfo = () => {
  if (__DEV__) {
    const { performance } = require('react-native');
    if (performance.memory) {
      return {
        usedJSHeapSize: performance.memory.usedJSHeapSize,
        totalJSHeapSize: performance.memory.totalJSHeapSize,
        jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
      };
    }
  }
  return null;
};

// Log memory periodically
setInterval(() => {
  const memory = getMemoryInfo();
  if (memory) {
    const usedMB = (memory.usedJSHeapSize / 1024 / 1024).toFixed(2);
    const totalMB = (memory.totalJSHeapSize / 1024 / 1024).toFixed(2);
    console.log(`Memory: ${usedMB}MB / ${totalMB}MB`);
  }
}, 5000);

Find Memory Leaks

import React, { useEffect, useRef } from 'react';

const MyComponent = () => {
  const intervalRef = useRef(null);
  
  useEffect(() => {
    // Good: cleanup on unmount
    intervalRef.current = setInterval(() => {
      console.log('Interval running');
    }, 1000);
    
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);
  
  return <View />;
};

Network Profiling

Monitor Network Requests

import { XMLHttpRequest } from 'react-native';

const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.open = function(method, url) {
  this._startTime = Date.now();
  this._url = url;
  this._method = method;
  return originalOpen.apply(this, arguments);
};

XMLHttpRequest.prototype.send = function() {
  this.addEventListener('load', function() {
    const duration = Date.now() - this._startTime;
    console.log(`${this._method} ${this._url}: ${duration}ms`);
  });
  return originalSend.apply(this, arguments);
};

Profiling Best Practices

  1. Profile on Real Devices: Simulators/emulators don’t represent real performance
  2. Use Production Builds: Development mode has overhead
  3. Test on Low-End Devices: Identify performance issues early
  4. Profile Specific Scenarios: Focus on critical user journeys
  5. Baseline First: Establish baseline metrics before optimization
  6. Profile Incrementally: Profile after each optimization

Profiling Checklist

  • Enable performance monitor
  • Profile with React DevTools
  • Use Hermes profiler for JavaScript
  • Profile native threads (Systrace/Instruments)
  • Monitor frame rate during animations
  • Track memory usage and leaks
  • Profile network requests
  • Test on multiple device tiers

Next Steps

Build docs developers (and LLMs) love