This guide covers the comprehensive performance monitoring system for acp-mobile.
The app includes a complete performance monitoring suite that tracks:
- Memory Usage - JS heap allocation, garbage collection, memory leaks
- Frame Rate (FPS) - Rendering performance, slow frames, UI jank
- Component Renders - React re-render tracking, optimization opportunities
- React Query - Cache behavior, query performance, network activity
Performance monitoring is automatically enabled in development mode. When you run:
npm startYou'll see in the console:
✅ QueryClient initialized with optimized settings
✅ why-did-you-render initialized
🔍 Performance monitoring active
📊 Type "performance.report()" in console for metrics
- Visual Dashboard: Tap the 📊 button in the bottom-right corner of the app
- Console Reports: Type
performance.report()in your development console - Individual Reports:
performance.memory.printReport()- Memory statsperformance.fps.printReport()- FPS statsperformance.render.printReport()- Render stats (not exposed yet)
Location: utils/performanceMonitor.ts
Tracks JavaScript heap memory usage to detect memory leaks and high usage.
Automatic Alerts:
⚠️ Warning at 75% memory usage- 🔴 Critical at 90% memory usage
- 🚨 Memory leak detection (sustained growth over 10 samples)
Configuration:
import { startMemoryMonitoring } from '@/utils/performanceMonitor'
const monitor = startMemoryMonitoring({
checkIntervalMs: 15000, // Check every 15 seconds
warningThreshold: 0.75, // Warn at 75%
criticalThreshold: 0.9, // Critical at 90%
onWarning: (stats) => {
// Custom warning handler
},
onCritical: (stats) => {
// Custom critical handler
},
})API:
const monitor = getMemoryMonitor()
// Get current stats
const stats = monitor.getCurrentStats()
// => { usedJSHeapSize, totalJSHeapSize, jsHeapSizeLimit, usagePercentage, timestamp }
// Get historical data
const history = monitor.getHistory()
// Detect memory leaks
const hasLeak = monitor.detectMemoryLeak()
// Print formatted report
monitor.printReport()
// Control monitoring
monitor.start()
monitor.stop()Expected Output:
📊 Memory Report
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Used: 45.3 MB
Total: 128.0 MB
Limit: 2048.0 MB
Usage: 35.4%
Samples: 25
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Location: utils/fpsMonitor.ts
Tracks frame rendering performance to identify UI jank and slow frames.
Target: 60 FPS (16.67ms per frame) Slow Frame Threshold: Below 45 FPS
Automatic Alerts:
- 🐌 Slow frame detected when FPS drops below 45
- 📉 FPS drop warning every second if below threshold
Configuration:
import { startFPSMonitoring } from '@/utils/fpsMonitor'
const monitor = startFPSMonitoring({
targetFPS: 60,
slowFrameThreshold: 45,
sampleSize: 60, // Number of frames to average
onSlowFrame: (fps, frameTime) => {
console.warn(`Slow frame: ${fps} FPS`)
},
onFPSDrop: (stats) => {
console.warn(`FPS drop: ${stats.current} FPS`)
},
})API:
const monitor = getFPSMonitor()
// Get current stats
const stats = monitor.getStats()
// => { current, average, min, max, slowFrameCount, totalFrames, timestamp }
// Check performance quality
const isGood = monitor.isPerformanceGood() // true if >50 FPS avg & <5% slow frames
// Get slow frame percentage
const slowPercent = monitor.getSlowFramePercentage()
// Print formatted report
monitor.printReport()
// Control monitoring
monitor.start()
monitor.stop()
monitor.reset()Expected Output:
📊 FPS Report
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Current: 58.3 FPS
Average: 59.1 FPS
Min: 45.2 FPS
Max: 60.0 FPS
Total Frames: 3542
Slow Frames: 23 (0.6%)
Performance: ✅ Good
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Location: utils/renderTracker.ts
Tracks component render counts and identifies unnecessary re-renders using why-did-you-render.
Automatic Tracking: All components are tracked automatically for render counts and timing.
Manual Opt-in for Detailed Analysis:
import { enableRenderTracking } from '@/utils/renderTracker'
// Enable why-did-you-render for specific component
const MyComponent = () => {
/* ... */
}
MyComponent.whyDidYouRender = true
// Or use the helper
enableRenderTracking(MyComponent, 'MyComponent')Hooks:
- Track Component Renders:
import { useRenderTracker } from '@/utils/renderTracker'
function MyComponent() {
const { renderCount, getStats } = useRenderTracker('MyComponent')
// renderCount increments on each render
// getStats() returns detailed metrics
}- Debug Prop Changes:
import { useWhyDidYouUpdate } from '@/utils/renderTracker'
function MyComponent(props) {
useWhyDidYouUpdate('MyComponent', props)
// Logs which props changed between renders
// Warns if component re-rendered with same props
}API:
const tracker = getRenderTracker()
// Get stats for a specific component
const stats = tracker.getComponentStats('MyComponent')
// => { componentName, renderCount, lastRenderTime, averageRenderTime, totalRenderTime }
// Get all tracked components
const allStats = tracker.getAllStats()
// Find problematic components
const excessive = tracker.getExcessiveRenders(50) // >50 renders
const slow = tracker.getSlowComponents(16) // >16ms average
// Print formatted report
tracker.printReport()
// Control tracking
tracker.reset()
tracker.resetComponent('MyComponent')Expected Output:
📊 React Render Report
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔄 Most Rendered Components:
1. Header: 234 renders (avg: 2.1ms)
2. SessionCard: 156 renders (avg: 5.3ms)
3. Dashboard: 89 renders (avg: 12.7ms)
⚠️ Components with Excessive Renders (>50):
ThemeProvider: 234 renders
Header: 234 renders
🐌 Slow Rendering Components (>16ms):
SessionsList: 18.5ms average
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Location: components/PerformanceMonitor.tsx
Real-time visual overlay showing all performance metrics.
Access: Tap the 📊 floating button in the bottom-right corner (development only)
Features:
- Real-time memory usage with progress bar
- Current/Average/Min/Max FPS
- Slow frame count and percentage
- Top 10 most-rendered components
- Print full report to console
- Reset all metrics
Color Coding:
- 🟢 Green: Good performance (<80% threshold)
- 🟠 Orange: Warning (80-90% threshold)
- 🔴 Red: Critical (>90% threshold)
Option 1: Use useRenderTracker hook
import { useRenderTracker } from '@/utils/renderTracker'
export function SessionCard({ session }: SessionCardProps) {
useRenderTracker('SessionCard')
// Your component logic
}Option 2: Enable why-did-you-render
import { memo } from 'react'
const SessionCard = memo(({ session }: SessionCardProps) => {
// Your component logic
})
// Enable detailed tracking
if (__DEV__) {
SessionCard.whyDidYouRender = true
}
export default SessionCardOption 3: Debug prop changes
import { useWhyDidYouUpdate } from '@/utils/renderTracker'
export function Header(props: HeaderProps) {
useWhyDidYouUpdate('Header', props)
// If Header re-renders unnecessarily, you'll see:
// "[Header] Re-rendered with same props (unnecessary re-render)"
// or
// "[Header] Props changed: { isRefetching: { from: false, to: true } }"
}The QueryClient has been configured with performance optimizations:
{
queries: {
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
refetchOnWindowFocus: false,
refetchOnReconnect: true,
staleTime: 5 * 60 * 1000, // 5 minutes - reduces unnecessary refetches
gcTime: 10 * 60 * 1000, // 10 minutes - automatic garbage collection
networkMode: 'offlineFirst', // Better offline support
}
}Singleton Pattern: QueryClient is created once and reused across renders, preventing:
- Memory leaks from multiple instances
- Cache loss on re-renders
- Unnecessary re-initialization
- Duplicate API requests
Before making changes:
// In development console
performance.report()Save the output for comparison.
Look for:
- Memory usage > 75%
- FPS < 50
- Components with >50 renders
- Components with >16ms average render time
For excessive renders:
// Add to component
useWhyDidYouUpdate('MyComponent', props)
// Check logs to see what's changingFor memory leaks:
const monitor = getMemoryMonitor()
const hasLeak = monitor.detectMemoryLeak()
if (hasLeak) {
// Check for:
// - Event listeners not cleaned up
// - Subscriptions not unsubscribed
// - Closures holding references
// - Large objects in state
}For slow renders:
// Add Profiler to component
import { Profiler } from 'react'
<Profiler id="MyComponent" onRender={(id, phase, actualDuration) => {
if (actualDuration > 16) {
console.warn(`Slow render in ${id}: ${actualDuration}ms`)
}
}}>
<MyComponent />
</Profiler>Based on the Performance Analysis Report, implement fixes.
// Reset metrics
performance.memory.reset?.()
performance.fps.reset()
// Use the app for a typical session
// Check new metrics
performance.report()Compare before/after metrics to quantify improvement.
Memoized Context Values:
const contextValue = useMemo(
() => ({ user, isAuthenticated, login, logout }),
[user, isAuthenticated, login, logout]
)
return <Context.Provider value={contextValue}>{children}</Context.Provider>Stable Callbacks:
const handlePress = useCallback(() => {
// Handler logic
}, [dependencies])Proper FlatList Configuration:
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
initialNumToRender={15}
maxToRenderPerBatch={15}
windowSize={7}
removeClippedSubviews={true}
/>Creating Objects in Render:
// Bad
<Provider value={{ user, login, logout }}>
// Good
const value = useMemo(() => ({ user, login, logout }), [user, login, logout])
<Provider value={value}>Inline Function Props:
// Bad
<Button onPress={() => handlePress(item.id)} />
// Good
const handlePress = useCallback((id) => {/* ... */}, [])
<Button onPress={() => handlePress(item.id)} />Missing Cleanup:
// Bad
useEffect(() => {
const subscription = subscribe()
// Missing cleanup!
}, [])
// Good
useEffect(() => {
const subscription = subscribe()
return () => subscription.unsubscribe()
}, [])Performance monitoring is disabled in production builds to avoid overhead.
To enable production monitoring:
// In app/_layout.tsx
const ENABLE_PROD_MONITORING = false // Set to true to enable
if (__DEV__ || ENABLE_PROD_MONITORING) {
// Monitoring code
}Warning: Production monitoring adds runtime overhead. Use sparingly and only for debugging specific issues.
Some metrics (like performance.memory) are only available in Chrome/V8-based environments. On iOS, memory stats may not be available.
Solution: Use Android emulator or Chrome debugger for full metrics.
Render tracking requires components to render at least once.
Solution: Navigate through the app to generate render data, then check metrics.
This is often normal as the app loads initial data and renders components.
Solution: Monitor for 30-60 seconds. If memory continues growing, investigate for leaks.
Expected during complex animations. Check if FPS recovers after animation completes.
Solution: Consider using react-native-reanimated for smoother animations on UI thread.
- ✅ Set up monitoring (you're here!)
- 📊 Collect baseline metrics - Run app and save
performance.report()output - 🔍 Review Performance Analysis - See Performance Analysis Report
- 🛠️ Implement optimizations - Apply P0 fixes first
- ✅ Verify improvements - Compare before/after metrics