Skip to main content
Profiling & Monitoring

Module Overview

Estimated Time: 4 hours | Difficulty: Advanced | Prerequisites: Performance Optimization, Debugging modules
Profiling and monitoring are essential for maintaining high-performance React Native applications. This module covers tools and techniques for identifying performance bottlenecks, monitoring production apps, and ensuring optimal user experience. What You’ll Learn:
  • React DevTools Profiler
  • Flipper performance plugins
  • Native profiling tools
  • Production monitoring setup
  • Custom performance metrics
  • APM integration

Performance Profiling Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Performance Profiling Stack                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   Development Profiling                 Production Monitoring                │
│   ─────────────────────                 ─────────────────────                │
│                                                                              │
│   ┌─────────────────────┐               ┌─────────────────────┐             │
│   │  React DevTools     │               │  Crash Reporting    │             │
│   │  - Component render │               │  - Sentry           │             │
│   │  - Re-render causes │               │  - Bugsnag          │             │
│   │  - Commit timing    │               │  - Firebase         │             │
│   └─────────────────────┘               └─────────────────────┘             │
│                                                                              │
│   ┌─────────────────────┐               ┌─────────────────────┐             │
│   │  Flipper            │               │  APM Tools          │             │
│   │  - Network          │               │  - New Relic        │             │
│   │  - Layout           │               │  - Datadog          │             │
│   │  - Performance      │               │  - Dynatrace        │             │
│   └─────────────────────┘               └─────────────────────┘             │
│                                                                              │
│   ┌─────────────────────┐               ┌─────────────────────┐             │
│   │  Native Profilers   │               │  Custom Metrics     │             │
│   │  - Xcode Instruments│               │  - App startup      │             │
│   │  - Android Profiler │               │  - Screen load      │             │
│   │  - Systrace         │               │  - API latency      │             │
│   └─────────────────────┘               └─────────────────────┘             │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

React DevTools Profiler

Setting Up Profiler

// Enable profiling in development
// The Profiler is available in React DevTools

// Wrap components to profile
import { 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>
  );
}

Custom Profiler Component

// components/PerformanceProfiler.tsx
import { Profiler, ProfilerOnRenderCallback, ReactNode } from 'react';

interface PerformanceProfilerProps {
  id: string;
  children: ReactNode;
  enabled?: boolean;
  threshold?: number; // Log only if render takes longer than threshold
}

const renderMetrics: Map<string, number[]> = new Map();

export function PerformanceProfiler({
  id,
  children,
  enabled = __DEV__,
  threshold = 16, // 16ms = 60fps
}: PerformanceProfilerProps) {
  const handleRender: ProfilerOnRenderCallback = (
    componentId,
    phase,
    actualDuration,
    baseDuration,
    startTime,
    commitTime
  ) => {
    // Store metrics
    const metrics = renderMetrics.get(componentId) || [];
    metrics.push(actualDuration);
    renderMetrics.set(componentId, metrics);

    // Log slow renders
    if (actualDuration > threshold) {
      console.warn(`[Slow Render] ${componentId}`, {
        phase,
        actualDuration: `${actualDuration.toFixed(2)}ms`,
        baseDuration: `${baseDuration.toFixed(2)}ms`,
        improvement: `${((baseDuration - actualDuration) / baseDuration * 100).toFixed(1)}%`,
      });
    }
  };

  if (!enabled) {
    return <>{children}</>;
  }

  return (
    <Profiler id={id} onRender={handleRender}>
      {children}
    </Profiler>
  );
}

// Get profiling statistics
export function getProfilingStats(componentId: string) {
  const metrics = renderMetrics.get(componentId) || [];
  if (metrics.length === 0) return null;

  const sum = metrics.reduce((a, b) => a + b, 0);
  const avg = sum / metrics.length;
  const max = Math.max(...metrics);
  const min = Math.min(...metrics);

  return {
    renderCount: metrics.length,
    averageTime: avg.toFixed(2),
    maxTime: max.toFixed(2),
    minTime: min.toFixed(2),
    totalTime: sum.toFixed(2),
  };
}

Flipper Integration

Setup Flipper

# Flipper is included in React Native CLI projects
# For Expo, use expo-dev-client

npx expo install expo-dev-client

Custom Flipper Plugin

// plugins/PerformancePlugin.ts
import { addPlugin } from 'react-native-flipper';

interface PerformanceEvent {
  type: 'render' | 'api' | 'navigation' | 'custom';
  name: string;
  duration: number;
  timestamp: number;
  metadata?: Record<string, unknown>;
}

let flipperConnection: any = null;

export function initPerformancePlugin() {
  addPlugin({
    getId() {
      return 'react-native-performance';
    },
    onConnect(connection) {
      flipperConnection = connection;
      console.log('Performance plugin connected');
    },
    onDisconnect() {
      flipperConnection = null;
    },
    runInBackground() {
      return true;
    },
  });
}

export function logPerformanceEvent(event: PerformanceEvent) {
  if (flipperConnection) {
    flipperConnection.send('performanceEvent', event);
  }
  
  // Also log to console in development
  if (__DEV__) {
    console.log(`[Performance] ${event.type}: ${event.name} - ${event.duration}ms`);
  }
}

Performance Tracking Hook

// hooks/usePerformanceTracking.ts
import { useEffect, useRef } from 'react';
import { logPerformanceEvent } from '@/plugins/PerformancePlugin';

export function usePerformanceTracking(screenName: string) {
  const startTime = useRef(Date.now());
  const hasLogged = useRef(false);

  useEffect(() => {
    // Track screen mount time
    const mountTime = Date.now() - startTime.current;
    
    if (!hasLogged.current) {
      logPerformanceEvent({
        type: 'navigation',
        name: `${screenName}_mount`,
        duration: mountTime,
        timestamp: Date.now(),
      });
      hasLogged.current = true;
    }

    return () => {
      // Track screen unmount
      logPerformanceEvent({
        type: 'navigation',
        name: `${screenName}_unmount`,
        duration: Date.now() - startTime.current,
        timestamp: Date.now(),
      });
    };
  }, [screenName]);

  // Return function to track custom events
  return (eventName: string, metadata?: Record<string, unknown>) => {
    logPerformanceEvent({
      type: 'custom',
      name: `${screenName}_${eventName}`,
      duration: Date.now() - startTime.current,
      timestamp: Date.now(),
      metadata,
    });
  };
}

Native Profiling Tools

iOS Instruments

// Enable Time Profiler in Xcode
// Product > Profile > Time Profiler

// Add signposts for custom measurements
import os.signpost

let log = OSLog(subsystem: "com.myapp", category: "Performance")

// Start measurement
os_signpost(.begin, log: log, name: "API Call")

// End measurement
os_signpost(.end, log: log, name: "API Call")

Android Profiler

// Enable Android Profiler in Android Studio
// View > Tool Windows > Profiler

// Add trace sections
import android.os.Trace

// Start trace
Trace.beginSection("API Call")

// End trace
Trace.endSection()

Systrace Integration

// Enable Systrace in React Native
// In development, shake device > "Start Systrace"

// Or programmatically
import { Systrace } from 'react-native';

// Start tracing
Systrace.beginEvent('MyCustomEvent');

// End tracing
Systrace.endEvent();

// Async tracing
const cookie = Systrace.beginAsyncEvent('AsyncOperation');
// ... async work
Systrace.endAsyncEvent('AsyncOperation', cookie);

Custom Performance Metrics

Performance Metrics Service

// services/performanceMetrics.ts
import { InteractionManager } from 'react-native';

interface Metric {
  name: string;
  value: number;
  unit: string;
  timestamp: number;
  tags?: Record<string, string>;
}

class PerformanceMetrics {
  private metrics: Metric[] = [];
  private timers: Map<string, number> = new Map();

  // Start a timer
  startTimer(name: string): void {
    this.timers.set(name, performance.now());
  }

  // End timer and record metric
  endTimer(name: string, tags?: Record<string, string>): number {
    const startTime = this.timers.get(name);
    if (!startTime) {
      console.warn(`Timer ${name} not found`);
      return 0;
    }

    const duration = performance.now() - startTime;
    this.timers.delete(name);

    this.recordMetric({
      name,
      value: duration,
      unit: 'ms',
      timestamp: Date.now(),
      tags,
    });

    return duration;
  }

  // Record a metric
  recordMetric(metric: Metric): void {
    this.metrics.push(metric);

    // Send to analytics after interactions complete
    InteractionManager.runAfterInteractions(() => {
      this.flushMetrics();
    });
  }

  // Flush metrics to backend
  private async flushMetrics(): Promise<void> {
    if (this.metrics.length === 0) return;

    const metricsToSend = [...this.metrics];
    this.metrics = [];

    try {
      await fetch('/api/metrics', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ metrics: metricsToSend }),
      });
    } catch (error) {
      // Re-add metrics on failure
      this.metrics = [...metricsToSend, ...this.metrics];
    }
  }

  // Get all recorded metrics
  getMetrics(): Metric[] {
    return [...this.metrics];
  }

  // Clear all metrics
  clearMetrics(): void {
    this.metrics = [];
    this.timers.clear();
  }
}

export const performanceMetrics = new PerformanceMetrics();

App Startup Tracking

// services/startupMetrics.ts
import { performanceMetrics } from './performanceMetrics';

interface StartupMetrics {
  coldStart: number;
  jsLoad: number;
  firstRender: number;
  interactive: number;
}

class StartupTracker {
  private appStartTime: number;
  private metrics: Partial<StartupMetrics> = {};

  constructor() {
    // Record app start time as early as possible
    this.appStartTime = Date.now();
  }

  markJsLoaded(): void {
    this.metrics.jsLoad = Date.now() - this.appStartTime;
    performanceMetrics.recordMetric({
      name: 'startup_js_load',
      value: this.metrics.jsLoad,
      unit: 'ms',
      timestamp: Date.now(),
    });
  }

  markFirstRender(): void {
    this.metrics.firstRender = Date.now() - this.appStartTime;
    performanceMetrics.recordMetric({
      name: 'startup_first_render',
      value: this.metrics.firstRender,
      unit: 'ms',
      timestamp: Date.now(),
    });
  }

  markInteractive(): void {
    this.metrics.interactive = Date.now() - this.appStartTime;
    this.metrics.coldStart = this.metrics.interactive;
    
    performanceMetrics.recordMetric({
      name: 'startup_interactive',
      value: this.metrics.interactive,
      unit: 'ms',
      timestamp: Date.now(),
    });

    console.log('Startup Metrics:', this.metrics);
  }

  getMetrics(): Partial<StartupMetrics> {
    return { ...this.metrics };
  }
}

export const startupTracker = new StartupTracker();

Usage in App Entry

// App.tsx
import { useEffect, useState } from 'react';
import { startupTracker } from '@/services/startupMetrics';

// Mark JS loaded immediately
startupTracker.markJsLoaded();

export default function App() {
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    // Mark first render
    startupTracker.markFirstRender();

    // Initialize app
    async function prepare() {
      // Load fonts, fetch initial data, etc.
      await loadResources();
      
      setIsReady(true);
      
      // Mark interactive after state update
      requestAnimationFrame(() => {
        startupTracker.markInteractive();
      });
    }

    prepare();
  }, []);

  if (!isReady) {
    return <SplashScreen />;
  }

  return <MainApp />;
}

APM Integration

Sentry Performance Monitoring

npx expo install @sentry/react-native
// services/sentry.ts
import * as Sentry from '@sentry/react-native';

export function initSentry() {
  Sentry.init({
    dsn: 'YOUR_SENTRY_DSN',
    
    // Performance monitoring
    tracesSampleRate: 1.0, // 100% in dev, lower in production
    
    // Enable automatic instrumentation
    enableAutoPerformanceTracing: true,
    
    // React Native specific
    enableNative: true,
    enableNativeCrashHandling: true,
    
    // Environment
    environment: __DEV__ ? 'development' : 'production',
    
    // Before send hook
    beforeSend(event) {
      // Filter or modify events
      return event;
    },
  });
}

// Wrap navigation container
export const SentryNavigationContainer = Sentry.wrap(NavigationContainer);

// Manual transaction
export function trackTransaction(name: string, operation: string) {
  const transaction = Sentry.startTransaction({
    name,
    op: operation,
  });

  return {
    finish: () => transaction.finish(),
    setTag: (key: string, value: string) => transaction.setTag(key, value),
    setData: (key: string, value: unknown) => transaction.setData(key, value),
  };
}

Custom Spans

// hooks/useTrackedFetch.ts
import * as Sentry from '@sentry/react-native';

export function useTrackedFetch() {
  return async function trackedFetch<T>(
    url: string,
    options?: RequestInit
  ): Promise<T> {
    const transaction = Sentry.getCurrentHub().getScope()?.getTransaction();
    
    const span = transaction?.startChild({
      op: 'http.client',
      description: `${options?.method || 'GET'} ${url}`,
    });

    try {
      const response = await fetch(url, options);
      
      span?.setHttpStatus(response.status);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      const data = await response.json();
      return data as T;
    } catch (error) {
      span?.setStatus('internal_error');
      throw error;
    } finally {
      span?.finish();
    }
  };
}

New Relic Integration

npm install newrelic-react-native-agent
// services/newrelic.ts
import NewRelic from 'newrelic-react-native-agent';

export function initNewRelic() {
  const appToken = Platform.select({
    ios: 'IOS_APP_TOKEN',
    android: 'ANDROID_APP_TOKEN',
  });

  NewRelic.startAgent(appToken);
  
  // Enable features
  NewRelic.setJSAppVersion('1.0.0');
  NewRelic.analyticsEventEnabled(true);
  NewRelic.networkRequestEnabled(true);
  NewRelic.networkErrorRequestEnabled(true);
  NewRelic.httpResponseBodyCaptureEnabled(true);
}

// Track custom events
export function trackEvent(name: string, attributes?: Record<string, unknown>) {
  NewRelic.recordCustomEvent('CustomEvent', name, attributes);
}

// Track breadcrumbs
export function trackBreadcrumb(name: string, attributes?: Record<string, unknown>) {
  NewRelic.recordBreadcrumb(name, attributes);
}

// Track errors
export function trackError(error: Error, attributes?: Record<string, unknown>) {
  NewRelic.recordError(error);
}

Frame Rate Monitoring

// hooks/useFrameRate.ts
import { useEffect, useRef, useState } from 'react';

interface FrameRateStats {
  currentFps: number;
  averageFps: number;
  droppedFrames: number;
  jankCount: number; // Frames taking > 16ms
}

export function useFrameRate(enabled: boolean = __DEV__): FrameRateStats {
  const [stats, setStats] = useState<FrameRateStats>({
    currentFps: 60,
    averageFps: 60,
    droppedFrames: 0,
    jankCount: 0,
  });

  const frameTimesRef = useRef<number[]>([]);
  const lastFrameTimeRef = useRef(performance.now());
  const frameIdRef = useRef<number>();

  useEffect(() => {
    if (!enabled) return;

    const measureFrame = () => {
      const now = performance.now();
      const delta = now - lastFrameTimeRef.current;
      lastFrameTimeRef.current = now;

      frameTimesRef.current.push(delta);

      // Keep last 60 frames
      if (frameTimesRef.current.length > 60) {
        frameTimesRef.current.shift();
      }

      // Calculate stats every 10 frames
      if (frameTimesRef.current.length % 10 === 0) {
        const frameTimes = frameTimesRef.current;
        const avgFrameTime = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;
        const currentFps = Math.round(1000 / delta);
        const averageFps = Math.round(1000 / avgFrameTime);
        const jankCount = frameTimes.filter(t => t > 16.67).length;
        const droppedFrames = frameTimes.filter(t => t > 33.33).length;

        setStats({
          currentFps,
          averageFps,
          droppedFrames,
          jankCount,
        });
      }

      frameIdRef.current = requestAnimationFrame(measureFrame);
    };

    frameIdRef.current = requestAnimationFrame(measureFrame);

    return () => {
      if (frameIdRef.current) {
        cancelAnimationFrame(frameIdRef.current);
      }
    };
  }, [enabled]);

  return stats;
}

FPS Monitor Component

// components/FPSMonitor.tsx
import { View, Text, StyleSheet } from 'react-native';
import { useFrameRate } from '@/hooks/useFrameRate';

export function FPSMonitor() {
  const { currentFps, averageFps, jankCount } = useFrameRate(true);

  const getFpsColor = (fps: number) => {
    if (fps >= 55) return '#22c55e'; // Green
    if (fps >= 30) return '#eab308'; // Yellow
    return '#ef4444'; // Red
  };

  if (!__DEV__) return null;

  return (
    <View style={styles.container}>
      <Text style={[styles.fps, { color: getFpsColor(currentFps) }]}>
        {currentFps} FPS
      </Text>
      <Text style={styles.details}>
        Avg: {averageFps} | Jank: {jankCount}
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    top: 50,
    right: 10,
    backgroundColor: 'rgba(0, 0, 0, 0.7)',
    padding: 8,
    borderRadius: 8,
    zIndex: 9999,
  },
  fps: {
    fontSize: 16,
    fontWeight: 'bold',
  },
  details: {
    fontSize: 10,
    color: '#9ca3af',
  },
});

Memory Monitoring

// hooks/useMemoryMonitor.ts
import { useEffect, useState } from 'react';
import { NativeModules, Platform } from 'react-native';

interface MemoryInfo {
  usedJSHeap: number;
  totalJSHeap: number;
  usedNativeHeap?: number;
}

export function useMemoryMonitor(intervalMs: number = 5000): MemoryInfo | null {
  const [memoryInfo, setMemoryInfo] = useState<MemoryInfo | null>(null);

  useEffect(() => {
    const getMemoryInfo = async () => {
      try {
        // JavaScript heap (available in Hermes)
        if (global.HermesInternal) {
          const heapInfo = global.HermesInternal.getRuntimeProperties();
          setMemoryInfo({
            usedJSHeap: heapInfo['Heap Size'] || 0,
            totalJSHeap: heapInfo['Heap Size Limit'] || 0,
          });
        }
      } catch (error) {
        console.warn('Memory monitoring not available:', error);
      }
    };

    getMemoryInfo();
    const interval = setInterval(getMemoryInfo, intervalMs);

    return () => clearInterval(interval);
  }, [intervalMs]);

  return memoryInfo;
}

Performance Dashboard

// screens/PerformanceDashboard.tsx
import { View, Text, ScrollView, StyleSheet } from 'react-native';
import { useFrameRate } from '@/hooks/useFrameRate';
import { useMemoryMonitor } from '@/hooks/useMemoryMonitor';
import { startupTracker } from '@/services/startupMetrics';
import { performanceMetrics } from '@/services/performanceMetrics';

export function PerformanceDashboard() {
  const frameRate = useFrameRate(true);
  const memory = useMemoryMonitor();
  const startup = startupTracker.getMetrics();
  const metrics = performanceMetrics.getMetrics();

  return (
    <ScrollView style={styles.container}>
      <Text style={styles.title}>Performance Dashboard</Text>

      {/* Frame Rate */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Frame Rate</Text>
        <MetricRow label="Current FPS" value={`${frameRate.currentFps}`} />
        <MetricRow label="Average FPS" value={`${frameRate.averageFps}`} />
        <MetricRow label="Dropped Frames" value={`${frameRate.droppedFrames}`} />
        <MetricRow label="Jank Count" value={`${frameRate.jankCount}`} />
      </View>

      {/* Memory */}
      {memory && (
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>Memory</Text>
          <MetricRow 
            label="JS Heap Used" 
            value={`${(memory.usedJSHeap / 1024 / 1024).toFixed(2)} MB`} 
          />
          <MetricRow 
            label="JS Heap Total" 
            value={`${(memory.totalJSHeap / 1024 / 1024).toFixed(2)} MB`} 
          />
        </View>
      )}

      {/* Startup */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Startup</Text>
        <MetricRow label="JS Load" value={`${startup.jsLoad || 0}ms`} />
        <MetricRow label="First Render" value={`${startup.firstRender || 0}ms`} />
        <MetricRow label="Interactive" value={`${startup.interactive || 0}ms`} />
      </View>

      {/* Recent Metrics */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Recent Metrics</Text>
        {metrics.slice(-10).map((metric, index) => (
          <MetricRow 
            key={index}
            label={metric.name} 
            value={`${metric.value.toFixed(2)}${metric.unit}`} 
          />
        ))}
      </View>
    </ScrollView>
  );
}

function MetricRow({ label, value }: { label: string; value: string }) {
  return (
    <View style={styles.metricRow}>
      <Text style={styles.metricLabel}>{label}</Text>
      <Text style={styles.metricValue}>{value}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f9fafb',
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 24,
    color: '#111827',
  },
  section: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.05,
    shadowRadius: 2,
    elevation: 1,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 12,
    color: '#374151',
  },
  metricRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#f3f4f6',
  },
  metricLabel: {
    fontSize: 14,
    color: '#6b7280',
  },
  metricValue: {
    fontSize: 14,
    fontWeight: '500',
    color: '#111827',
  },
});

Best Practices

Profile in Production Mode

Development mode adds overhead - always profile release builds

Sample in Production

Don’t collect 100% of metrics in production - use sampling

Set Performance Budgets

Define acceptable thresholds and alert when exceeded

Monitor Real Users

Synthetic tests don’t capture real-world conditions

Next Steps

Module 36: Error Handling & Crash Reporting

Learn to handle errors gracefully and report crashes effectively