diff --git a/docs/PERFORMANCE_MONITORING.md b/docs/PERFORMANCE_MONITORING.md
new file mode 100644
index 00000000..10f1c632
--- /dev/null
+++ b/docs/PERFORMANCE_MONITORING.md
@@ -0,0 +1,147 @@
+# Performance Monitoring
+
+TeachLink Mobile ships with a layered performance monitoring stack that covers component
+render timing, Core Web Vitals, crash reporting, and production analytics.
+
+---
+
+## Architecture Overview
+
+```
+AnalyticsProvider (root)
+├── mobileAnalyticsService – event ingestion & sampling
+├── crashReportingService – global JS / promise error handlers
+└── webVitalsService – LCP, FID, CLS, FCP, TTFB (web target)
+
+useReactProfiler (hook)
+└── Profiler (React built-in)
+ └── ProfiledScreen (wrapper component)
+```
+
+---
+
+## Metrics Definitions
+
+### React Profiler Metrics
+
+| Metric | Definition | Unit |
+|---|---|---|
+| `render_duration` | `actualDuration` reported by React's `Profiler` — wall-clock time spent rendering the committed subtree | ms |
+| Slow render | Any render where `actualDuration > slowRenderThresholdMs` (default 16 ms / 1 frame at 60 fps) | — |
+| `averageRenderDurationMs` | Rolling mean of the last `maxSamples` (default 100) render durations | ms |
+
+### Core Web Vitals (web target)
+
+| Metric | Good | Needs Improvement | Poor | Description |
+|---|---|---|---|---|
+| LCP | ≤ 2500 ms | ≤ 4000 ms | > 4000 ms | Largest Contentful Paint |
+| FID | ≤ 100 ms | ≤ 300 ms | > 300 ms | First Input Delay |
+| CLS | ≤ 0.1 | ≤ 0.25 | > 0.25 | Cumulative Layout Shift |
+| FCP | ≤ 1800 ms | ≤ 3000 ms | > 3000 ms | First Contentful Paint |
+| TTFB | ≤ 800 ms | ≤ 1800 ms | > 1800 ms | Time to First Byte |
+
+Thresholds are sourced from Google's Core Web Vitals recommendations and defined in
+`src/services/webVitals.ts`.
+
+### Navigation & Infrastructure
+
+| Metric | Key | Description |
+|---|---|---|
+| Navigation latency | `navigation_latency` | Time between user tap and screen mount |
+| App load time | `app_load_time` | Cold-start JS bundle load |
+| Screen transition time | `screen_transition_time` | Animated navigation transition |
+| API response time | `api_response_time` | Round-trip HTTP call |
+| Time to interactive | `time_to_interactive` | App ready for full interaction |
+
+---
+
+## Usage
+
+### Wrap a screen with `ProfiledScreen`
+
+```tsx
+import { ProfiledScreen } from '@/components/mobile/ProfiledScreen';
+
+export default function HomeScreen() {
+ return (
+
+
+
+ );
+}
+```
+
+Every render is forwarded to `mobileAnalyticsService.trackPerformance`. Renders slower
+than 16 ms also emit a `PERFORMANCE_METRIC` event with `is_slow: true` and a console
+warning (visible in Metro logs).
+
+### Use `useReactProfiler` directly
+
+Use this when you need access to the live `metrics` object (e.g. a dev-only overlay):
+
+```tsx
+import { Profiler } from 'react';
+import { useReactProfiler } from '@/hooks/useReactProfiler';
+
+function MyComponent() {
+ const { onRender, metrics } = useReactProfiler('MyComponent', {
+ slowRenderThresholdMs: 32, // 2 frames
+ maxSamples: 50,
+ });
+
+ return (
+
+ {/* your subtree */}
+
+ );
+}
+```
+
+### Initialise web vitals (automatic)
+
+`webVitalsService.init()` is called inside `AnalyticsProvider` on app mount. No extra
+setup is required. On React Native / non-browser environments the `web-vitals` functions
+are no-ops, so the call is safe everywhere.
+
+---
+
+## Performance Baselines
+
+Baselines are committed to `performance-baseline.json`. Update them after intentional
+performance changes:
+
+```bash
+npm run perf:update-baseline
+git add performance-baseline.json
+git commit -m "perf: update baseline after "
+```
+
+CI regresses on any metric that worsens by > 5% vs the stored baseline (see
+`docs/PERFORMANCE_THRESHOLDS.md`).
+
+---
+
+## Alerts & Regression Detection
+
+### Slow-render alert
+
+Logged to Metro console and sent as a `PERFORMANCE_METRIC` analytics event whenever a
+single render exceeds `slowRenderThresholdMs`.
+
+### Web Vitals regression
+
+`webVitalsService` stores the first reading of each metric as a baseline. Subsequent
+readings > 20% above baseline emit a `WEB_VITALS_REGRESSION` analytics event.
+
+### Production crash threshold
+
+`crashReportingService` counts unhandled JS errors. When the count reaches
+`MAX_ERRORS_THRESHOLD` (5) it calls `alertProductionIssue`, which logs a `PRODUCTION
+ALERT` warning. In a real deployment this would fan out to a Slack channel or PagerDuty.
+
+---
+
+## Related Documents
+
+- [PERFORMANCE_TESTING.md](./PERFORMANCE_TESTING.md) — component-level perf test guide
+- [PERFORMANCE_THRESHOLDS.md](./PERFORMANCE_THRESHOLDS.md) — CI budget & regression gate
diff --git a/src/components/mobile/AnalyticsProvider.tsx b/src/components/mobile/AnalyticsProvider.tsx
index 5db8a1cc..521b9fed 100644
--- a/src/components/mobile/AnalyticsProvider.tsx
+++ b/src/components/mobile/AnalyticsProvider.tsx
@@ -2,6 +2,7 @@ import React, { createContext, ReactNode, useContext, useEffect, useRef } from '
import { AppState, AppStateStatus } from 'react-native';
import { crashReportingService } from '../../services/crashReporting';
import { mobileAnalyticsService } from '../../services/mobileAnalytics';
+import webVitalsService from '../../services/webVitals';
import logger from '../../utils/logger';
import { ErrorBoundary } from '../common/ErrorBoundary';
@@ -24,9 +25,10 @@ export const AnalyticsProvider: React.FC = ({ children }
useEffect(() => {
// 1. Initialize services on mount
- logger.info('📱 [AnalyticsProvider] Initializing tracking and crash reporting...');
+ logger.info('📱 [AnalyticsProvider] Initializing tracking, crash reporting and web vitals...');
mobileAnalyticsService.init();
crashReportingService.init();
+ webVitalsService.init();
// 2. Manage session lifecycle (Foreground vs. Background)
const handleAppStateChange = (nextAppState: AppStateStatus) => {
diff --git a/src/components/mobile/ProfiledScreen.tsx b/src/components/mobile/ProfiledScreen.tsx
new file mode 100644
index 00000000..39f96894
--- /dev/null
+++ b/src/components/mobile/ProfiledScreen.tsx
@@ -0,0 +1,32 @@
+import React, { Profiler, ReactNode } from 'react';
+import { useReactProfiler, ProfilerOptions } from '../../hooks/useReactProfiler';
+
+interface ProfiledScreenProps {
+ name: string;
+ children: ReactNode;
+ options?: ProfilerOptions;
+}
+
+/**
+ * Wraps a screen (or any subtree) with React's built-in Profiler and forwards
+ * render-timing data to analytics via `useReactProfiler`.
+ *
+ * Only active when `__DEV__` is true or when the profiler is explicitly enabled
+ * via the options prop, keeping production overhead negligible.
+ *
+ * Usage:
+ *
+ *
+ *
+ */
+export const ProfiledScreen: React.FC = ({ name, children, options }) => {
+ const { onRender } = useReactProfiler(name, options);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default ProfiledScreen;
diff --git a/src/components/mobile/index.ts b/src/components/mobile/index.ts
index 535196e2..6081cdaf 100644
--- a/src/components/mobile/index.ts
+++ b/src/components/mobile/index.ts
@@ -31,4 +31,5 @@ export * from './SwipeableCoordinator';
export * from './SwipeableRow';
export * from './VirtualList';
export * from './VoiceSearch';
+export * from './ProfiledScreen';
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 43adc866..0ccf80e5 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -37,4 +37,5 @@ export { OptimizedVideoGesturesView, useOptimizedVideoGestures } from './useOpti
export * from './useDebounce';
export * from './useHealthDashboard';
export * from './usePredictivePreload';
+export * from './useReactProfiler';
diff --git a/src/hooks/useReactProfiler.ts b/src/hooks/useReactProfiler.ts
new file mode 100644
index 00000000..416ca820
--- /dev/null
+++ b/src/hooks/useReactProfiler.ts
@@ -0,0 +1,112 @@
+import { useCallback, useRef } from 'react';
+import { mobileAnalyticsService } from '../services/mobileAnalytics';
+import { AnalyticsEvent, PerformanceMetric } from '../utils/trackingEvents';
+import { appLogger } from '../utils/logger';
+
+export interface ProfilerMetrics {
+ componentName: string;
+ renderCount: number;
+ lastRenderDurationMs: number;
+ lastCommitDurationMs: number;
+ averageRenderDurationMs: number;
+ slowRenders: number;
+}
+
+export interface ProfilerOptions {
+ slowRenderThresholdMs?: number;
+ maxSamples?: number;
+}
+
+export type ProfilerOnRenderCallback = (
+ id: string,
+ phase: 'mount' | 'update' | 'nested-update',
+ actualDuration: number,
+ baseDuration: number,
+ startTime: number,
+ commitTime: number
+) => void;
+
+const DEFAULT_SLOW_THRESHOLD_MS = 16;
+const DEFAULT_MAX_SAMPLES = 100;
+
+/**
+ * Provides a React Profiler `onRender` callback that records render timing
+ * metrics and forwards slow renders to the analytics service.
+ *
+ * Usage:
+ * const { onRender, metrics } = useReactProfiler('MyScreen');
+ * …
+ */
+export function useReactProfiler(
+ componentName: string,
+ options: ProfilerOptions = {}
+): { onRender: ProfilerOnRenderCallback; metrics: ProfilerMetrics } {
+ const { slowRenderThresholdMs = DEFAULT_SLOW_THRESHOLD_MS, maxSamples = DEFAULT_MAX_SAMPLES } =
+ options;
+
+ const renderCount = useRef(0);
+ const totalDuration = useRef(0);
+ const slowRenders = useRef(0);
+ const lastRenderDurationMs = useRef(0);
+ const lastCommitDurationMs = useRef(0);
+ const samples = useRef([]);
+
+ const onRender = useCallback(
+ (_id, phase, actualDuration, _baseDuration, _startTime, commitTime) => {
+ renderCount.current += 1;
+ lastRenderDurationMs.current = actualDuration;
+ lastCommitDurationMs.current = commitTime;
+
+ samples.current.push(actualDuration);
+ if (samples.current.length > maxSamples) {
+ samples.current.shift();
+ }
+ totalDuration.current += actualDuration;
+
+ if (actualDuration > slowRenderThresholdMs) {
+ slowRenders.current += 1;
+
+ appLogger.infoSync(
+ `[ReactProfiler] Slow render detected — ${componentName} (${phase}): ${actualDuration.toFixed(2)}ms`
+ );
+
+ mobileAnalyticsService.trackEvent(AnalyticsEvent.PERFORMANCE_METRIC, {
+ metric_name: PerformanceMetric.RENDER_DURATION,
+ component: componentName,
+ phase,
+ actual_duration_ms: Math.round(actualDuration),
+ commit_time: commitTime,
+ is_slow: true,
+ });
+ }
+
+ mobileAnalyticsService.trackPerformance(PerformanceMetric.RENDER_DURATION, actualDuration, {
+ component: componentName,
+ phase,
+ render_count: renderCount.current,
+ event_category: 'high_frequency',
+ event_name: `profiler_${componentName}`,
+ });
+ },
+ [componentName, slowRenderThresholdMs, maxSamples]
+ );
+
+ const sampleArr = samples.current;
+ const avg =
+ sampleArr.length > 0
+ ? sampleArr.reduce((a, b) => a + b, 0) / sampleArr.length
+ : 0;
+
+ const metrics: ProfilerMetrics = {
+ componentName,
+ renderCount: renderCount.current,
+ lastRenderDurationMs: lastRenderDurationMs.current,
+ lastCommitDurationMs: lastCommitDurationMs.current,
+ averageRenderDurationMs: avg,
+ slowRenders: slowRenders.current,
+ };
+
+ return { onRender, metrics };
+}
+
+export default useReactProfiler;
diff --git a/src/utils/trackingEvents.ts b/src/utils/trackingEvents.ts
index 1cbc37b8..e2f289d1 100644
--- a/src/utils/trackingEvents.ts
+++ b/src/utils/trackingEvents.ts
@@ -36,6 +36,8 @@ export enum AnalyticsEvent {
// Performance & Infrastructure
PERFORMANCE_METRIC = 'performance_metric',
+ REACT_PROFILER_RENDER = 'react_profiler_render',
+ REACT_PROFILER_SLOW_RENDER = 'react_profiler_slow_render',
AB_ASSIGNMENT = 'ab_assignment',
AB_EXPOSURE = 'ab_exposure',
API_ERROR = 'api_error',
@@ -83,6 +85,10 @@ export enum PerformanceMetric {
SCREEN_TRANSITION_TIME = 'screen_transition_time',
API_RESPONSE_TIME = 'api_response_time',
+ // React Profiler
+ RENDER_DURATION = 'render_duration',
+ NAVIGATION_LATENCY = 'navigation_latency',
+
// Core Web Vitals
LCP = 'lcp',
FID = 'fid',
diff --git a/tests/components/ProfiledScreen.test.tsx b/tests/components/ProfiledScreen.test.tsx
new file mode 100644
index 00000000..037c76c0
--- /dev/null
+++ b/tests/components/ProfiledScreen.test.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { Text } from 'react-native';
+import { render } from '@testing-library/react-native';
+import { ProfiledScreen } from '../../src/components/mobile/ProfiledScreen';
+import { mobileAnalyticsService } from '../../src/services/mobileAnalytics';
+
+jest.mock('../../src/services/mobileAnalytics', () => ({
+ mobileAnalyticsService: {
+ trackEvent: jest.fn(),
+ trackPerformance: jest.fn(),
+ },
+}));
+
+jest.mock('../../src/utils/logger', () => ({
+ appLogger: {
+ infoSync: jest.fn(),
+ warnSync: jest.fn(),
+ },
+}));
+
+const mockTrackPerformance = mobileAnalyticsService.trackPerformance as jest.MockedFunction<
+ typeof mobileAnalyticsService.trackPerformance
+>;
+
+describe('ProfiledScreen', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders children without crashing', () => {
+ const { getByText } = render(
+
+ Hello World
+
+ );
+
+ expect(getByText('Hello World')).toBeTruthy();
+ });
+
+ it('forwards render metrics to analytics on mount', () => {
+ render(
+
+ Content
+
+ );
+
+ expect(mockTrackPerformance).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.any(Number),
+ expect.objectContaining({ component: 'HomeScreen' })
+ );
+ });
+
+ it('passes the screen name as Profiler id', () => {
+ render(
+
+ Course
+
+ );
+
+ expect(mockTrackPerformance).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.any(Number),
+ expect.objectContaining({ component: 'CourseScreen' })
+ );
+ });
+
+ it('accepts profiler options without crashing', () => {
+ expect(() =>
+ render(
+
+ Options
+
+ )
+ ).not.toThrow();
+ });
+});
diff --git a/tests/hooks/useReactProfiler.test.ts b/tests/hooks/useReactProfiler.test.ts
new file mode 100644
index 00000000..72958a08
--- /dev/null
+++ b/tests/hooks/useReactProfiler.test.ts
@@ -0,0 +1,141 @@
+import { renderHook, act } from '@testing-library/react-native';
+import { useReactProfiler } from '../../src/hooks/useReactProfiler';
+import { mobileAnalyticsService } from '../../src/services/mobileAnalytics';
+import { appLogger } from '../../src/utils/logger';
+import { PerformanceMetric } from '../../src/utils/trackingEvents';
+
+jest.mock('../../src/services/mobileAnalytics', () => ({
+ mobileAnalyticsService: {
+ trackEvent: jest.fn(),
+ trackPerformance: jest.fn(),
+ },
+}));
+
+jest.mock('../../src/utils/logger', () => ({
+ appLogger: {
+ infoSync: jest.fn(),
+ warnSync: jest.fn(),
+ },
+}));
+
+const mockTrackEvent = mobileAnalyticsService.trackEvent as jest.MockedFunction<
+ typeof mobileAnalyticsService.trackEvent
+>;
+const mockTrackPerformance = mobileAnalyticsService.trackPerformance as jest.MockedFunction<
+ typeof mobileAnalyticsService.trackPerformance
+>;
+const mockInfoSync = appLogger.infoSync as jest.MockedFunction;
+
+function invokeOnRender(
+ hook: ReturnType,
+ overrides: Partial<{
+ id: string;
+ phase: 'mount' | 'update' | 'nested-update';
+ actualDuration: number;
+ baseDuration: number;
+ startTime: number;
+ commitTime: number;
+ }> = {}
+) {
+ const {
+ id = 'TestScreen',
+ phase = 'mount',
+ actualDuration = 5,
+ baseDuration = 5,
+ startTime = 0,
+ commitTime = 10,
+ } = overrides;
+ act(() => {
+ hook.onRender(id, phase, actualDuration, baseDuration, startTime, commitTime);
+ });
+}
+
+describe('useReactProfiler', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns onRender callback and initial metrics', () => {
+ const { result } = renderHook(() => useReactProfiler('HomeScreen'));
+
+ expect(typeof result.current.onRender).toBe('function');
+ expect(result.current.metrics.componentName).toBe('HomeScreen');
+ expect(result.current.metrics.renderCount).toBe(0);
+ expect(result.current.metrics.slowRenders).toBe(0);
+ });
+
+ it('calls trackPerformance on every render', () => {
+ const { result } = renderHook(() => useReactProfiler('HomeScreen'));
+
+ invokeOnRender(result.current, { actualDuration: 8 });
+
+ expect(mockTrackPerformance).toHaveBeenCalledWith(
+ PerformanceMetric.RENDER_DURATION,
+ 8,
+ expect.objectContaining({ component: 'HomeScreen', phase: 'mount' })
+ );
+ });
+
+ it('does NOT flag a fast render as slow', () => {
+ const { result } = renderHook(() =>
+ useReactProfiler('HomeScreen', { slowRenderThresholdMs: 16 })
+ );
+
+ invokeOnRender(result.current, { actualDuration: 10 });
+
+ expect(mockTrackEvent).not.toHaveBeenCalled();
+ expect(mockInfoSync).not.toHaveBeenCalled();
+ });
+
+ it('logs and tracks a slow render via trackEvent', () => {
+ const { result } = renderHook(() =>
+ useReactProfiler('SlowScreen', { slowRenderThresholdMs: 16 })
+ );
+
+ invokeOnRender(result.current, { actualDuration: 50, phase: 'update' });
+
+ expect(mockInfoSync).toHaveBeenCalledWith(
+ expect.stringContaining('Slow render detected — SlowScreen (update): 50.00ms')
+ );
+ expect(mockTrackEvent).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ component: 'SlowScreen',
+ phase: 'update',
+ is_slow: true,
+ })
+ );
+ });
+
+ it('respects a custom slowRenderThresholdMs option', () => {
+ const { result } = renderHook(() =>
+ useReactProfiler('FastScreen', { slowRenderThresholdMs: 100 })
+ );
+
+ invokeOnRender(result.current, { actualDuration: 50 });
+
+ expect(mockTrackEvent).not.toHaveBeenCalled();
+ });
+
+ it('accumulates render count across multiple renders', () => {
+ const { result } = renderHook(() => useReactProfiler('Counter'));
+
+ invokeOnRender(result.current);
+ invokeOnRender(result.current, { phase: 'update' });
+ invokeOnRender(result.current, { phase: 'update' });
+
+ expect(mockTrackPerformance).toHaveBeenCalledTimes(3);
+ });
+
+ it('passes event_category high_frequency to suppress sampling', () => {
+ const { result } = renderHook(() => useReactProfiler('SampledScreen'));
+
+ invokeOnRender(result.current);
+
+ expect(mockTrackPerformance).toHaveBeenCalledWith(
+ PerformanceMetric.RENDER_DURATION,
+ expect.any(Number),
+ expect.objectContaining({ event_category: 'high_frequency' })
+ );
+ });
+});