Module Overview
Estimated Time: 4 hours | Difficulty: Advanced | Prerequisites: Performance Optimization, Debugging modules
- React DevTools Profiler
- Flipper performance plugins
- Native profiling tools
- Production monitoring setup
- Custom performance metrics
- APM integration
Performance Profiling Overview
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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
Copy
// 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
Copy
// 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
Copy
# Flipper is included in React Native CLI projects
# For Expo, use expo-dev-client
npx expo install expo-dev-client
Custom Flipper Plugin
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
npx expo install @sentry/react-native
Copy
// 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
Copy
// 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
Copy
npm install newrelic-react-native-agent
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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