> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 35. Profiling & Monitoring

> Performance profiling and production monitoring for React Native applications

<Frame>
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/devweeekends/images/courses/react-native-crash-course/profiling.svg" alt="Profiling & Monitoring" />
</Frame>

## Module Overview

<Info>
  **Estimated Time**: 4 hours | **Difficulty**: Advanced | **Prerequisites**: Performance Optimization, Debugging modules
</Info>

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:**

* 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

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.

### Setting Up Profiler

```tsx theme={null}
// 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

```tsx theme={null}
// 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

```bash theme={null}
# Flipper is included in React Native CLI projects
# For Expo, use expo-dev-client

npx expo install expo-dev-client
```

### Custom Flipper Plugin

```tsx theme={null}
// 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

```tsx theme={null}
// 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

```swift theme={null}
// 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

```kotlin theme={null}
// 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

```tsx theme={null}
// 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

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).

### Performance Metrics Service

```tsx theme={null}
// 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

```tsx theme={null}
// 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

```tsx theme={null}
// 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

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.

### Sentry Performance Monitoring

```bash theme={null}
npx expo install @sentry/react-native
```

```tsx theme={null}
// 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

```tsx theme={null}
// 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

```bash theme={null}
npm install newrelic-react-native-agent
```

```tsx theme={null}
// 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

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.

```tsx theme={null}
// 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

```tsx theme={null}
// 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

```tsx theme={null}
// 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

```tsx theme={null}
// 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',
  },
});
```

***

## Choosing the Right Profiling Tool

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.

| Tool                                  | What It Measures                                                 | When to Use                                                               | Limitations                                                       |
| ------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| **React DevTools Profiler**           | Component render count, render duration, re-render causes        | JS thread feels slow; lists are sluggish; state changes cause visible lag | Cannot see native-side issues; dev mode overhead inflates numbers |
| **Flipper (Performance plugin)**      | Network timing, layout hierarchy, custom events                  | General debugging; inspecting network waterfall; layout issues            | Adds its own overhead; not available in Expo Go                   |
| **Xcode Instruments (Time Profiler)** | CPU usage per function on native thread                          | iOS animations stutter; app startup is slow; native module is suspect     | macOS only; requires archive/release build for accurate data      |
| **Android Profiler**                  | CPU, memory, network, energy on native thread                    | Android-specific jank; memory leaks; battery drain investigation          | Requires Android Studio; emulator profiling is inaccurate         |
| **Systrace**                          | Frame-by-frame timeline across JS and native threads             | Need to see the bridge; diagnosing cross-thread bottlenecks               | Low-level; hard to interpret without experience                   |
| **Custom Performance Metrics**        | Anything you instrument (startup time, screen load, API latency) | Production monitoring; tracking real user experience over time            | Only measures what you instrument; adds code complexity           |
| **Sentry / APM**                      | Transactions, spans, crash correlation, real-user performance    | Production observability; understanding P50/P95 latencies across devices  | Cost at scale; sampling means you miss some events                |

**Decision framework -- where to start:**

1. **"The app feels slow"** -- Start with React DevTools Profiler. Most perceived slowness in React Native comes from unnecessary re-renders on the JS thread.
2. **"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.
3. **"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.
4. **"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.
5. **"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.

***

## Hermes vs. JSC Profiling Differences

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:

```tsx theme={null}
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
}
```

***

## Performance Budgets

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.

| Metric                        | Budget (Good)        | Budget (Acceptable) | Red Flag     |
| ----------------------------- | -------------------- | ------------------- | ------------ |
| **Cold start to interactive** | Under 1.5s           | Under 3s            | Over 5s      |
| **Screen transition**         | Under 300ms          | Under 500ms         | Over 1s      |
| **FlatList scroll FPS**       | 60 FPS sustained     | 45+ FPS sustained   | Below 30 FPS |
| **API response render**       | Under 200ms (cached) | Under 1s (network)  | Over 3s      |
| **JS bundle size**            | Under 2 MB           | Under 5 MB          | Over 10 MB   |
| **Memory usage (idle)**       | Under 100 MB         | Under 200 MB        | Over 300 MB  |
| **TTI after deep link**       | Under 2s             | Under 4s            | Over 6s      |

**Enforcing budgets in CI:**

```tsx theme={null}
// scripts/check-bundle-size.ts
import { statSync } from 'fs';

const BUNDLE_SIZE_LIMIT = 5 * 1024 * 1024; // 5 MB
const bundlePath = 'android/app/build/generated/assets/react/release/index.android.bundle';

const stats = statSync(bundlePath);
if (stats.size > BUNDLE_SIZE_LIMIT) {
  console.error(
    `Bundle size ${(stats.size / 1024 / 1024).toFixed(2)} MB ` +
    `exceeds limit of ${(BUNDLE_SIZE_LIMIT / 1024 / 1024).toFixed(2)} MB`
  );
  process.exit(1);
}
```

***

## Common Profiling Pitfalls

### Profiling in Development Mode

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:

```bash theme={null}
# Build a production-like profile for profiling
npx expo run:ios --configuration Release
npx expo run:android --variant release
```

### The Observer Effect

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.

### Misattributing Bridge Overhead

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.

### Memory Leak False Positives

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.

***

## Best Practices

<CardGroup cols={2}>
  <Card title="Profile in Production Mode" icon="rocket">
    Development mode adds overhead - always profile release builds
  </Card>

  <Card title="Sample in Production" icon="chart-line">
    Don't collect 100% of metrics in production - use sampling
  </Card>

  <Card title="Set Performance Budgets" icon="gauge">
    Define acceptable thresholds and alert when exceeded
  </Card>

  <Card title="Monitor Real Users" icon="users">
    Synthetic tests don't capture real-world conditions
  </Card>
</CardGroup>

### Production Sampling Strategy

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.

***

## Next Steps

<Card title="Module 36: Error Handling & Crash Reporting" icon="arrow-right" href="/courses/react-native-crash-course/36-error-handling">
  Learn to handle errors gracefully and report crashes effectively
</Card>
