@croacroa/react-native-template 2.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -0
- package/.eslintrc.js +8 -0
- package/.github/workflows/ci.yml +187 -187
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/.github/workflows/npm-publish.yml +57 -0
- package/CHANGELOG.md +195 -106
- package/CONTRIBUTING.md +377 -377
- package/LICENSE +21 -21
- package/README.md +446 -402
- package/__tests__/accessibility/components.test.tsx +285 -0
- package/__tests__/components/Button.test.tsx +2 -4
- package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
- package/__tests__/components/snapshots.test.tsx +131 -131
- package/__tests__/helpers/a11y.ts +54 -0
- package/__tests__/hooks/useAnalytics.test.ts +100 -0
- package/__tests__/hooks/useAnimations.test.ts +70 -0
- package/__tests__/hooks/useAuth.test.tsx +71 -28
- package/__tests__/hooks/useMedia.test.ts +318 -0
- package/__tests__/hooks/usePayments.test.tsx +307 -0
- package/__tests__/hooks/usePermission.test.ts +230 -0
- package/__tests__/hooks/useWebSocket.test.ts +329 -0
- package/__tests__/integration/auth-api.test.tsx +224 -227
- package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
- package/__tests__/services/api.test.ts +24 -6
- package/app/(auth)/home.tsx +11 -9
- package/app/(auth)/profile.tsx +8 -6
- package/app/(auth)/settings.tsx +11 -9
- package/app/(public)/forgot-password.tsx +25 -15
- package/app/(public)/login.tsx +48 -12
- package/app/(public)/onboarding.tsx +5 -5
- package/app/(public)/register.tsx +24 -15
- package/app/_layout.tsx +6 -3
- package/app.config.ts +27 -2
- package/assets/images/.gitkeep +7 -7
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/notification-icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/components/ErrorBoundary.tsx +73 -28
- package/components/auth/SocialLoginButtons.tsx +168 -0
- package/components/forms/FormInput.tsx +5 -3
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/AnalyticsProvider.tsx +67 -0
- package/components/providers/SuspenseBoundary.tsx +359 -357
- package/components/providers/index.ts +24 -21
- package/components/ui/AnimatedButton.tsx +1 -9
- package/components/ui/AnimatedList.tsx +98 -0
- package/components/ui/AnimatedScreen.tsx +89 -0
- package/components/ui/Avatar.tsx +319 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Button.tsx +11 -3
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/FeatureGate.tsx +57 -0
- package/components/ui/ForceUpdateScreen.tsx +108 -0
- package/components/ui/ImagePickerButton.tsx +180 -0
- package/components/ui/Input.stories.tsx +2 -10
- package/components/ui/Input.tsx +2 -10
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Paywall.tsx +253 -0
- package/components/ui/PermissionGate.tsx +155 -0
- package/components/ui/PurchaseButton.tsx +84 -0
- package/components/ui/Select.tsx +240 -240
- package/components/ui/Skeleton.tsx +3 -1
- package/components/ui/Toast.tsx +427 -418
- package/components/ui/UploadProgress.tsx +189 -0
- package/components/ui/VirtualizedList.tsx +288 -285
- package/components/ui/index.ts +28 -30
- package/constants/config.ts +135 -97
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/docs/guides/analytics-posthog.md +121 -0
- package/docs/guides/auth-supabase.md +162 -0
- package/docs/guides/feature-flags-launchdarkly.md +150 -0
- package/docs/guides/payments-revenuecat.md +169 -0
- package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
- package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
- package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
- package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
- package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
- package/eas.json +2 -1
- package/hooks/index.ts +70 -40
- package/hooks/useAnimatedEntry.ts +204 -0
- package/hooks/useApi.ts +5 -4
- package/hooks/useAuth.tsx +7 -3
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useChannel.ts +111 -0
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useExperiment.ts +36 -0
- package/hooks/useFeatureFlag.ts +59 -0
- package/hooks/useForceUpdate.ts +91 -0
- package/hooks/useImagePicker.ts +281 -375
- package/hooks/useInAppReview.ts +64 -0
- package/hooks/useMFA.ts +509 -499
- package/hooks/useParallax.ts +142 -0
- package/hooks/usePerformance.ts +434 -434
- package/hooks/usePermission.ts +190 -0
- package/hooks/usePresence.ts +129 -0
- package/hooks/useProducts.ts +36 -0
- package/hooks/usePurchase.ts +103 -0
- package/hooks/useRateLimit.ts +70 -0
- package/hooks/useSubscription.ts +49 -0
- package/hooks/useTrackEvent.ts +52 -0
- package/hooks/useTrackScreen.ts +40 -0
- package/hooks/useUpdates.ts +358 -358
- package/hooks/useUpload.ts +165 -0
- package/hooks/useWebSocket.ts +111 -0
- package/i18n/index.ts +197 -194
- package/i18n/locales/ar.json +170 -101
- package/i18n/locales/de.json +170 -101
- package/i18n/locales/en.json +170 -101
- package/i18n/locales/es.json +170 -101
- package/i18n/locales/fr.json +170 -101
- package/jest.config.js +1 -1
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -92
- package/maestro/flows/mfa-setup.yaml +86 -86
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -101
- package/maestro/flows/offline-sync.yaml +128 -128
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +188 -176
- package/scripts/generate-placeholders.js +38 -0
- package/services/analytics/adapters/console.ts +50 -0
- package/services/analytics/analytics-adapter.ts +94 -0
- package/services/analytics/types.ts +73 -0
- package/services/analytics.ts +428 -428
- package/services/api.ts +419 -340
- package/services/auth/social/apple.ts +110 -0
- package/services/auth/social/google.ts +159 -0
- package/services/auth/social/social-auth.ts +100 -0
- package/services/auth/social/types.ts +80 -0
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +652 -626
- package/services/feature-flags/adapters/mock.ts +108 -0
- package/services/feature-flags/feature-flag-adapter.ts +174 -0
- package/services/feature-flags/types.ts +79 -0
- package/services/force-update.ts +140 -0
- package/services/index.ts +116 -54
- package/services/media/compression.ts +91 -0
- package/services/media/media-picker.ts +151 -0
- package/services/media/media-upload.ts +160 -0
- package/services/payments/adapters/mock.ts +159 -0
- package/services/payments/payment-adapter.ts +118 -0
- package/services/payments/types.ts +131 -0
- package/services/permissions/permission-manager.ts +284 -0
- package/services/permissions/types.ts +104 -0
- package/services/realtime/types.ts +100 -0
- package/services/realtime/websocket-manager.ts +441 -0
- package/services/security.ts +289 -286
- package/services/sentry.ts +4 -4
- package/stores/appStore.ts +9 -0
- package/stores/notificationStore.ts +3 -1
- package/tailwind.config.js +47 -47
- package/tsconfig.json +37 -13
- package/types/user.ts +1 -1
- package/utils/accessibility.ts +446 -446
- package/utils/animations/presets.ts +182 -0
- package/utils/animations/transitions.ts +62 -0
- package/utils/index.ts +63 -52
- package/utils/toast.ts +9 -2
- package/utils/validation.ts +4 -1
- package/utils/withAccessibility.tsx +272 -272
package/hooks/usePerformance.ts
CHANGED
|
@@ -1,434 +1,434 @@
|
|
|
1
|
-
import { useEffect, useRef, useCallback, useState } from "react";
|
|
2
|
-
import { InteractionManager } from "react-native";
|
|
3
|
-
import { IS_DEV, ENABLE_ANALYTICS } from "@/constants/config";
|
|
4
|
-
import { analytics, AnalyticsEvents } from "@/services/analytics";
|
|
5
|
-
|
|
6
|
-
// ============================================================================
|
|
7
|
-
// Types
|
|
8
|
-
// ============================================================================
|
|
9
|
-
|
|
10
|
-
interface PerformanceMetrics {
|
|
11
|
-
/**
|
|
12
|
-
* Time since component mounted (ms)
|
|
13
|
-
*/
|
|
14
|
-
mountTime: number;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Time for initial render (ms)
|
|
18
|
-
*/
|
|
19
|
-
renderTime: number;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Number of re-renders
|
|
23
|
-
*/
|
|
24
|
-
renderCount: number;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Last render duration (ms)
|
|
28
|
-
*/
|
|
29
|
-
lastRenderDuration: number;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Average FPS (if tracking enabled)
|
|
33
|
-
*/
|
|
34
|
-
fps: number;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Memory usage (if available)
|
|
38
|
-
*/
|
|
39
|
-
memoryUsage: number | null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface UsePerformanceOptions {
|
|
43
|
-
/**
|
|
44
|
-
* Name for identifying this component in logs
|
|
45
|
-
*/
|
|
46
|
-
name: string;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Enable FPS tracking
|
|
50
|
-
* @default false
|
|
51
|
-
*/
|
|
52
|
-
trackFps?: boolean;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Log metrics to console in development
|
|
56
|
-
* @default true
|
|
57
|
-
*/
|
|
58
|
-
logInDev?: boolean;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Report metrics to analytics
|
|
62
|
-
* @default false
|
|
63
|
-
*/
|
|
64
|
-
reportToAnalytics?: boolean;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Threshold for slow render warning (ms)
|
|
68
|
-
* @default 16
|
|
69
|
-
*/
|
|
70
|
-
slowRenderThreshold?: number;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface UsePerformanceReturn {
|
|
74
|
-
/**
|
|
75
|
-
* Current performance metrics
|
|
76
|
-
*/
|
|
77
|
-
metrics: PerformanceMetrics;
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Mark the start of an operation
|
|
81
|
-
*/
|
|
82
|
-
markStart: (name: string) => void;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Mark the end of an operation and get duration
|
|
86
|
-
*/
|
|
87
|
-
markEnd: (name: string) => number;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Track a custom metric
|
|
91
|
-
*/
|
|
92
|
-
trackMetric: (name: string, value: number) => void;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Reset all metrics
|
|
96
|
-
*/
|
|
97
|
-
reset: () => void;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ============================================================================
|
|
101
|
-
// Hook Implementation
|
|
102
|
-
// ============================================================================
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Hook for tracking component performance
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* ```tsx
|
|
109
|
-
* function MyScreen() {
|
|
110
|
-
* const { metrics, markStart, markEnd } = usePerformance({
|
|
111
|
-
* name: 'MyScreen',
|
|
112
|
-
* trackFps: true,
|
|
113
|
-
* });
|
|
114
|
-
*
|
|
115
|
-
* const fetchData = async () => {
|
|
116
|
-
* markStart('fetchData');
|
|
117
|
-
* const data = await api.get('/data');
|
|
118
|
-
* const duration = markEnd('fetchData');
|
|
119
|
-
* console.log(`Fetch took ${duration}ms`);
|
|
120
|
-
* };
|
|
121
|
-
*
|
|
122
|
-
* return <View />;
|
|
123
|
-
* }
|
|
124
|
-
* ```
|
|
125
|
-
*/
|
|
126
|
-
export function usePerformance(
|
|
127
|
-
options: UsePerformanceOptions
|
|
128
|
-
): UsePerformanceReturn {
|
|
129
|
-
const {
|
|
130
|
-
name,
|
|
131
|
-
trackFps = false,
|
|
132
|
-
logInDev = true,
|
|
133
|
-
reportToAnalytics = false,
|
|
134
|
-
slowRenderThreshold = 16, // 60fps = 16.67ms per frame
|
|
135
|
-
} = options;
|
|
136
|
-
|
|
137
|
-
const mountTimeRef = useRef(Date.now());
|
|
138
|
-
const renderCountRef = useRef(0);
|
|
139
|
-
const lastRenderStartRef = useRef(Date.now());
|
|
140
|
-
const marksRef = useRef<Map<string, number>>(new Map());
|
|
141
|
-
const fpsRef = useRef(60);
|
|
142
|
-
const frameCountRef = useRef(0);
|
|
143
|
-
const lastFpsUpdateRef = useRef(Date.now());
|
|
144
|
-
|
|
145
|
-
const [metrics, setMetrics] = useState<PerformanceMetrics>({
|
|
146
|
-
mountTime: 0,
|
|
147
|
-
renderTime: 0,
|
|
148
|
-
renderCount: 0,
|
|
149
|
-
lastRenderDuration: 0,
|
|
150
|
-
fps: 60,
|
|
151
|
-
memoryUsage: null,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Track render count and duration
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
const renderEnd = Date.now();
|
|
157
|
-
const renderDuration = renderEnd - lastRenderStartRef.current;
|
|
158
|
-
renderCountRef.current += 1;
|
|
159
|
-
|
|
160
|
-
// Update metrics
|
|
161
|
-
setMetrics((prev) => ({
|
|
162
|
-
...prev,
|
|
163
|
-
renderCount: renderCountRef.current,
|
|
164
|
-
lastRenderDuration: renderDuration,
|
|
165
|
-
renderTime:
|
|
166
|
-
renderCountRef.current === 1 ? renderDuration : prev.renderTime,
|
|
167
|
-
}));
|
|
168
|
-
|
|
169
|
-
// Warn on slow render
|
|
170
|
-
if (renderDuration > slowRenderThreshold && IS_DEV && logInDev) {
|
|
171
|
-
console.warn(
|
|
172
|
-
`[Performance] ${name}: Slow render detected (${renderDuration.toFixed(1)}ms)`
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Prepare for next render
|
|
177
|
-
lastRenderStartRef.current = Date.now();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Track mount time
|
|
181
|
-
useEffect(() => {
|
|
182
|
-
const mountDuration = Date.now() - mountTimeRef.current;
|
|
183
|
-
|
|
184
|
-
setMetrics((prev) => ({
|
|
185
|
-
...prev,
|
|
186
|
-
mountTime: mountDuration,
|
|
187
|
-
}));
|
|
188
|
-
|
|
189
|
-
if (IS_DEV && logInDev) {
|
|
190
|
-
console.log(`[Performance] ${name}: Mounted in ${mountDuration}ms`);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Report to analytics
|
|
194
|
-
if (reportToAnalytics && ENABLE_ANALYTICS) {
|
|
195
|
-
analytics.track(AnalyticsEvents.SCREEN_VIEW, {
|
|
196
|
-
screen: name,
|
|
197
|
-
mountTime: mountDuration,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return () => {
|
|
202
|
-
if (IS_DEV && logInDev) {
|
|
203
|
-
console.log(
|
|
204
|
-
`[Performance] ${name}: Unmounted after ${renderCountRef.current} renders`
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
}, [name, logInDev, reportToAnalytics]);
|
|
209
|
-
|
|
210
|
-
// FPS tracking
|
|
211
|
-
useEffect(() => {
|
|
212
|
-
if (!trackFps) return;
|
|
213
|
-
|
|
214
|
-
let animationFrameId: number;
|
|
215
|
-
let isRunning = true;
|
|
216
|
-
|
|
217
|
-
const trackFrame = () => {
|
|
218
|
-
if (!isRunning) return;
|
|
219
|
-
|
|
220
|
-
frameCountRef.current += 1;
|
|
221
|
-
const now = Date.now();
|
|
222
|
-
const elapsed = now - lastFpsUpdateRef.current;
|
|
223
|
-
|
|
224
|
-
// Update FPS every second
|
|
225
|
-
if (elapsed >= 1000) {
|
|
226
|
-
fpsRef.current = Math.round((frameCountRef.current * 1000) / elapsed);
|
|
227
|
-
frameCountRef.current = 0;
|
|
228
|
-
lastFpsUpdateRef.current = now;
|
|
229
|
-
|
|
230
|
-
setMetrics((prev) => ({
|
|
231
|
-
...prev,
|
|
232
|
-
fps: fpsRef.current,
|
|
233
|
-
}));
|
|
234
|
-
|
|
235
|
-
// Warn on low FPS
|
|
236
|
-
if (fpsRef.current < 30 && IS_DEV && logInDev) {
|
|
237
|
-
console.warn(
|
|
238
|
-
`[Performance] ${name}: Low FPS detected (${fpsRef.current})`
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
animationFrameId = requestAnimationFrame(trackFrame);
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
animationFrameId = requestAnimationFrame(trackFrame);
|
|
247
|
-
|
|
248
|
-
return () => {
|
|
249
|
-
isRunning = false;
|
|
250
|
-
cancelAnimationFrame(animationFrameId);
|
|
251
|
-
};
|
|
252
|
-
}, [trackFps, name, logInDev]);
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Mark the start of an operation
|
|
256
|
-
*/
|
|
257
|
-
const markStart = useCallback((markName: string) => {
|
|
258
|
-
marksRef.current.set(markName, Date.now());
|
|
259
|
-
}, []);
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Mark the end of an operation and get duration
|
|
263
|
-
*/
|
|
264
|
-
const markEnd = useCallback(
|
|
265
|
-
(markName: string): number => {
|
|
266
|
-
const start = marksRef.current.get(markName);
|
|
267
|
-
if (!start) {
|
|
268
|
-
console.warn(`[Performance] No start mark found for "${markName}"`);
|
|
269
|
-
return 0;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const duration = Date.now() - start;
|
|
273
|
-
marksRef.current.delete(markName);
|
|
274
|
-
|
|
275
|
-
if (IS_DEV && logInDev) {
|
|
276
|
-
console.log(`[Performance] ${name}.${markName}: ${duration}ms`);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (reportToAnalytics && ENABLE_ANALYTICS) {
|
|
280
|
-
analytics.track("Performance Metric", {
|
|
281
|
-
component: name,
|
|
282
|
-
operation: markName,
|
|
283
|
-
duration,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return duration;
|
|
288
|
-
},
|
|
289
|
-
[name, logInDev, reportToAnalytics]
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Track a custom metric
|
|
294
|
-
*/
|
|
295
|
-
const trackMetric = useCallback(
|
|
296
|
-
(metricName: string, value: number) => {
|
|
297
|
-
if (IS_DEV && logInDev) {
|
|
298
|
-
console.log(`[Performance] ${name}.${metricName}: ${value}`);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (reportToAnalytics && ENABLE_ANALYTICS) {
|
|
302
|
-
analytics.track("Performance Metric", {
|
|
303
|
-
component: name,
|
|
304
|
-
metric: metricName,
|
|
305
|
-
value,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
},
|
|
309
|
-
[name, logInDev, reportToAnalytics]
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Reset all metrics
|
|
314
|
-
*/
|
|
315
|
-
const reset = useCallback(() => {
|
|
316
|
-
mountTimeRef.current = Date.now();
|
|
317
|
-
renderCountRef.current = 0;
|
|
318
|
-
lastRenderStartRef.current = Date.now();
|
|
319
|
-
marksRef.current.clear();
|
|
320
|
-
|
|
321
|
-
setMetrics({
|
|
322
|
-
mountTime: 0,
|
|
323
|
-
renderTime: 0,
|
|
324
|
-
renderCount: 0,
|
|
325
|
-
lastRenderDuration: 0,
|
|
326
|
-
fps: 60,
|
|
327
|
-
memoryUsage: null,
|
|
328
|
-
});
|
|
329
|
-
}, []);
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
metrics,
|
|
333
|
-
markStart,
|
|
334
|
-
markEnd,
|
|
335
|
-
trackMetric,
|
|
336
|
-
reset,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ============================================================================
|
|
341
|
-
// Utility Functions
|
|
342
|
-
// ============================================================================
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Measure the execution time of an async function
|
|
346
|
-
*/
|
|
347
|
-
export async function measureAsync<T>(
|
|
348
|
-
name: string,
|
|
349
|
-
fn: () => Promise<T>,
|
|
350
|
-
options?: { log?: boolean; reportToAnalytics?: boolean }
|
|
351
|
-
): Promise<{ result: T; duration: number }> {
|
|
352
|
-
const { log = IS_DEV, reportToAnalytics = false } = options || {};
|
|
353
|
-
|
|
354
|
-
const start = Date.now();
|
|
355
|
-
const result = await fn();
|
|
356
|
-
const duration = Date.now() - start;
|
|
357
|
-
|
|
358
|
-
if (log) {
|
|
359
|
-
console.log(`[Performance] ${name}: ${duration}ms`);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (reportToAnalytics && ENABLE_ANALYTICS) {
|
|
363
|
-
analytics.track("Performance Metric", {
|
|
364
|
-
operation: name,
|
|
365
|
-
duration,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return { result, duration };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Measure the execution time of a sync function
|
|
374
|
-
*/
|
|
375
|
-
export function measureSync<T>(
|
|
376
|
-
name: string,
|
|
377
|
-
fn: () => T,
|
|
378
|
-
options?: { log?: boolean; reportToAnalytics?: boolean }
|
|
379
|
-
): { result: T; duration: number } {
|
|
380
|
-
const { log = IS_DEV, reportToAnalytics = false } = options || {};
|
|
381
|
-
|
|
382
|
-
const start = Date.now();
|
|
383
|
-
const result = fn();
|
|
384
|
-
const duration = Date.now() - start;
|
|
385
|
-
|
|
386
|
-
if (log) {
|
|
387
|
-
console.log(`[Performance] ${name}: ${duration}ms`);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (reportToAnalytics && ENABLE_ANALYTICS) {
|
|
391
|
-
analytics.track("Performance Metric", {
|
|
392
|
-
operation: name,
|
|
393
|
-
duration,
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return { result, duration };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Wait for interactions to complete before executing
|
|
402
|
-
* Useful for deferring heavy operations
|
|
403
|
-
*/
|
|
404
|
-
export function runAfterInteractions<T>(fn: () => T | Promise<T>): Promise<T> {
|
|
405
|
-
return new Promise((resolve) => {
|
|
406
|
-
InteractionManager.runAfterInteractions(async () => {
|
|
407
|
-
const result = await fn();
|
|
408
|
-
resolve(result);
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Create a debounced function with performance tracking
|
|
415
|
-
*/
|
|
416
|
-
export function createTrackedDebounce<
|
|
417
|
-
T extends (...args: unknown[]) => unknown,
|
|
418
|
-
>(fn: T, delay: number, name: string): T {
|
|
419
|
-
let timeoutId: NodeJS.Timeout;
|
|
420
|
-
let callCount = 0;
|
|
421
|
-
|
|
422
|
-
return ((...args: Parameters<T>) => {
|
|
423
|
-
callCount++;
|
|
424
|
-
clearTimeout(timeoutId);
|
|
425
|
-
|
|
426
|
-
timeoutId = setTimeout(() => {
|
|
427
|
-
if (IS_DEV) {
|
|
428
|
-
console.log(`[Performance] ${name}: Debounced ${callCount} calls to 1`);
|
|
429
|
-
}
|
|
430
|
-
callCount = 0;
|
|
431
|
-
fn(...args);
|
|
432
|
-
}, delay);
|
|
433
|
-
}) as T;
|
|
434
|
-
}
|
|
1
|
+
import { useEffect, useRef, useCallback, useState } from "react";
|
|
2
|
+
import { InteractionManager } from "react-native";
|
|
3
|
+
import { IS_DEV, ENABLE_ANALYTICS } from "@/constants/config";
|
|
4
|
+
import { analytics, AnalyticsEvents } from "@/services/analytics";
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
interface PerformanceMetrics {
|
|
11
|
+
/**
|
|
12
|
+
* Time since component mounted (ms)
|
|
13
|
+
*/
|
|
14
|
+
mountTime: number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Time for initial render (ms)
|
|
18
|
+
*/
|
|
19
|
+
renderTime: number;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Number of re-renders
|
|
23
|
+
*/
|
|
24
|
+
renderCount: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Last render duration (ms)
|
|
28
|
+
*/
|
|
29
|
+
lastRenderDuration: number;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Average FPS (if tracking enabled)
|
|
33
|
+
*/
|
|
34
|
+
fps: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Memory usage (if available)
|
|
38
|
+
*/
|
|
39
|
+
memoryUsage: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface UsePerformanceOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Name for identifying this component in logs
|
|
45
|
+
*/
|
|
46
|
+
name: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Enable FPS tracking
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
trackFps?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Log metrics to console in development
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
logInDev?: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Report metrics to analytics
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
reportToAnalytics?: boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Threshold for slow render warning (ms)
|
|
68
|
+
* @default 16
|
|
69
|
+
*/
|
|
70
|
+
slowRenderThreshold?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface UsePerformanceReturn {
|
|
74
|
+
/**
|
|
75
|
+
* Current performance metrics
|
|
76
|
+
*/
|
|
77
|
+
metrics: PerformanceMetrics;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Mark the start of an operation
|
|
81
|
+
*/
|
|
82
|
+
markStart: (name: string) => void;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mark the end of an operation and get duration
|
|
86
|
+
*/
|
|
87
|
+
markEnd: (name: string) => number;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Track a custom metric
|
|
91
|
+
*/
|
|
92
|
+
trackMetric: (name: string, value: number) => void;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Reset all metrics
|
|
96
|
+
*/
|
|
97
|
+
reset: () => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Hook Implementation
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Hook for tracking component performance
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```tsx
|
|
109
|
+
* function MyScreen() {
|
|
110
|
+
* const { metrics, markStart, markEnd } = usePerformance({
|
|
111
|
+
* name: 'MyScreen',
|
|
112
|
+
* trackFps: true,
|
|
113
|
+
* });
|
|
114
|
+
*
|
|
115
|
+
* const fetchData = async () => {
|
|
116
|
+
* markStart('fetchData');
|
|
117
|
+
* const data = await api.get('/data');
|
|
118
|
+
* const duration = markEnd('fetchData');
|
|
119
|
+
* console.log(`Fetch took ${duration}ms`);
|
|
120
|
+
* };
|
|
121
|
+
*
|
|
122
|
+
* return <View />;
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function usePerformance(
|
|
127
|
+
options: UsePerformanceOptions
|
|
128
|
+
): UsePerformanceReturn {
|
|
129
|
+
const {
|
|
130
|
+
name,
|
|
131
|
+
trackFps = false,
|
|
132
|
+
logInDev = true,
|
|
133
|
+
reportToAnalytics = false,
|
|
134
|
+
slowRenderThreshold = 16, // 60fps = 16.67ms per frame
|
|
135
|
+
} = options;
|
|
136
|
+
|
|
137
|
+
const mountTimeRef = useRef(Date.now());
|
|
138
|
+
const renderCountRef = useRef(0);
|
|
139
|
+
const lastRenderStartRef = useRef(Date.now());
|
|
140
|
+
const marksRef = useRef<Map<string, number>>(new Map());
|
|
141
|
+
const fpsRef = useRef(60);
|
|
142
|
+
const frameCountRef = useRef(0);
|
|
143
|
+
const lastFpsUpdateRef = useRef(Date.now());
|
|
144
|
+
|
|
145
|
+
const [metrics, setMetrics] = useState<PerformanceMetrics>({
|
|
146
|
+
mountTime: 0,
|
|
147
|
+
renderTime: 0,
|
|
148
|
+
renderCount: 0,
|
|
149
|
+
lastRenderDuration: 0,
|
|
150
|
+
fps: 60,
|
|
151
|
+
memoryUsage: null,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Track render count and duration
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const renderEnd = Date.now();
|
|
157
|
+
const renderDuration = renderEnd - lastRenderStartRef.current;
|
|
158
|
+
renderCountRef.current += 1;
|
|
159
|
+
|
|
160
|
+
// Update metrics
|
|
161
|
+
setMetrics((prev) => ({
|
|
162
|
+
...prev,
|
|
163
|
+
renderCount: renderCountRef.current,
|
|
164
|
+
lastRenderDuration: renderDuration,
|
|
165
|
+
renderTime:
|
|
166
|
+
renderCountRef.current === 1 ? renderDuration : prev.renderTime,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
// Warn on slow render
|
|
170
|
+
if (renderDuration > slowRenderThreshold && IS_DEV && logInDev) {
|
|
171
|
+
console.warn(
|
|
172
|
+
`[Performance] ${name}: Slow render detected (${renderDuration.toFixed(1)}ms)`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Prepare for next render
|
|
177
|
+
lastRenderStartRef.current = Date.now();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Track mount time
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
const mountDuration = Date.now() - mountTimeRef.current;
|
|
183
|
+
|
|
184
|
+
setMetrics((prev) => ({
|
|
185
|
+
...prev,
|
|
186
|
+
mountTime: mountDuration,
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
if (IS_DEV && logInDev) {
|
|
190
|
+
console.log(`[Performance] ${name}: Mounted in ${mountDuration}ms`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Report to analytics
|
|
194
|
+
if (reportToAnalytics && ENABLE_ANALYTICS) {
|
|
195
|
+
analytics.track(AnalyticsEvents.SCREEN_VIEW, {
|
|
196
|
+
screen: name,
|
|
197
|
+
mountTime: mountDuration,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return () => {
|
|
202
|
+
if (IS_DEV && logInDev) {
|
|
203
|
+
console.log(
|
|
204
|
+
`[Performance] ${name}: Unmounted after ${renderCountRef.current} renders`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}, [name, logInDev, reportToAnalytics]);
|
|
209
|
+
|
|
210
|
+
// FPS tracking
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!trackFps) return;
|
|
213
|
+
|
|
214
|
+
let animationFrameId: number;
|
|
215
|
+
let isRunning = true;
|
|
216
|
+
|
|
217
|
+
const trackFrame = () => {
|
|
218
|
+
if (!isRunning) return;
|
|
219
|
+
|
|
220
|
+
frameCountRef.current += 1;
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const elapsed = now - lastFpsUpdateRef.current;
|
|
223
|
+
|
|
224
|
+
// Update FPS every second
|
|
225
|
+
if (elapsed >= 1000) {
|
|
226
|
+
fpsRef.current = Math.round((frameCountRef.current * 1000) / elapsed);
|
|
227
|
+
frameCountRef.current = 0;
|
|
228
|
+
lastFpsUpdateRef.current = now;
|
|
229
|
+
|
|
230
|
+
setMetrics((prev) => ({
|
|
231
|
+
...prev,
|
|
232
|
+
fps: fpsRef.current,
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
// Warn on low FPS
|
|
236
|
+
if (fpsRef.current < 30 && IS_DEV && logInDev) {
|
|
237
|
+
console.warn(
|
|
238
|
+
`[Performance] ${name}: Low FPS detected (${fpsRef.current})`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
animationFrameId = requestAnimationFrame(trackFrame);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
animationFrameId = requestAnimationFrame(trackFrame);
|
|
247
|
+
|
|
248
|
+
return () => {
|
|
249
|
+
isRunning = false;
|
|
250
|
+
cancelAnimationFrame(animationFrameId);
|
|
251
|
+
};
|
|
252
|
+
}, [trackFps, name, logInDev]);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Mark the start of an operation
|
|
256
|
+
*/
|
|
257
|
+
const markStart = useCallback((markName: string) => {
|
|
258
|
+
marksRef.current.set(markName, Date.now());
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Mark the end of an operation and get duration
|
|
263
|
+
*/
|
|
264
|
+
const markEnd = useCallback(
|
|
265
|
+
(markName: string): number => {
|
|
266
|
+
const start = marksRef.current.get(markName);
|
|
267
|
+
if (!start) {
|
|
268
|
+
console.warn(`[Performance] No start mark found for "${markName}"`);
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const duration = Date.now() - start;
|
|
273
|
+
marksRef.current.delete(markName);
|
|
274
|
+
|
|
275
|
+
if (IS_DEV && logInDev) {
|
|
276
|
+
console.log(`[Performance] ${name}.${markName}: ${duration}ms`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (reportToAnalytics && ENABLE_ANALYTICS) {
|
|
280
|
+
analytics.track("Performance Metric", {
|
|
281
|
+
component: name,
|
|
282
|
+
operation: markName,
|
|
283
|
+
duration,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return duration;
|
|
288
|
+
},
|
|
289
|
+
[name, logInDev, reportToAnalytics]
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Track a custom metric
|
|
294
|
+
*/
|
|
295
|
+
const trackMetric = useCallback(
|
|
296
|
+
(metricName: string, value: number) => {
|
|
297
|
+
if (IS_DEV && logInDev) {
|
|
298
|
+
console.log(`[Performance] ${name}.${metricName}: ${value}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (reportToAnalytics && ENABLE_ANALYTICS) {
|
|
302
|
+
analytics.track("Performance Metric", {
|
|
303
|
+
component: name,
|
|
304
|
+
metric: metricName,
|
|
305
|
+
value,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
[name, logInDev, reportToAnalytics]
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Reset all metrics
|
|
314
|
+
*/
|
|
315
|
+
const reset = useCallback(() => {
|
|
316
|
+
mountTimeRef.current = Date.now();
|
|
317
|
+
renderCountRef.current = 0;
|
|
318
|
+
lastRenderStartRef.current = Date.now();
|
|
319
|
+
marksRef.current.clear();
|
|
320
|
+
|
|
321
|
+
setMetrics({
|
|
322
|
+
mountTime: 0,
|
|
323
|
+
renderTime: 0,
|
|
324
|
+
renderCount: 0,
|
|
325
|
+
lastRenderDuration: 0,
|
|
326
|
+
fps: 60,
|
|
327
|
+
memoryUsage: null,
|
|
328
|
+
});
|
|
329
|
+
}, []);
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
metrics,
|
|
333
|
+
markStart,
|
|
334
|
+
markEnd,
|
|
335
|
+
trackMetric,
|
|
336
|
+
reset,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ============================================================================
|
|
341
|
+
// Utility Functions
|
|
342
|
+
// ============================================================================
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Measure the execution time of an async function
|
|
346
|
+
*/
|
|
347
|
+
export async function measureAsync<T>(
|
|
348
|
+
name: string,
|
|
349
|
+
fn: () => Promise<T>,
|
|
350
|
+
options?: { log?: boolean; reportToAnalytics?: boolean }
|
|
351
|
+
): Promise<{ result: T; duration: number }> {
|
|
352
|
+
const { log = IS_DEV, reportToAnalytics = false } = options || {};
|
|
353
|
+
|
|
354
|
+
const start = Date.now();
|
|
355
|
+
const result = await fn();
|
|
356
|
+
const duration = Date.now() - start;
|
|
357
|
+
|
|
358
|
+
if (log) {
|
|
359
|
+
console.log(`[Performance] ${name}: ${duration}ms`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (reportToAnalytics && ENABLE_ANALYTICS) {
|
|
363
|
+
analytics.track("Performance Metric", {
|
|
364
|
+
operation: name,
|
|
365
|
+
duration,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { result, duration };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Measure the execution time of a sync function
|
|
374
|
+
*/
|
|
375
|
+
export function measureSync<T>(
|
|
376
|
+
name: string,
|
|
377
|
+
fn: () => T,
|
|
378
|
+
options?: { log?: boolean; reportToAnalytics?: boolean }
|
|
379
|
+
): { result: T; duration: number } {
|
|
380
|
+
const { log = IS_DEV, reportToAnalytics = false } = options || {};
|
|
381
|
+
|
|
382
|
+
const start = Date.now();
|
|
383
|
+
const result = fn();
|
|
384
|
+
const duration = Date.now() - start;
|
|
385
|
+
|
|
386
|
+
if (log) {
|
|
387
|
+
console.log(`[Performance] ${name}: ${duration}ms`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (reportToAnalytics && ENABLE_ANALYTICS) {
|
|
391
|
+
analytics.track("Performance Metric", {
|
|
392
|
+
operation: name,
|
|
393
|
+
duration,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { result, duration };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Wait for interactions to complete before executing
|
|
402
|
+
* Useful for deferring heavy operations
|
|
403
|
+
*/
|
|
404
|
+
export function runAfterInteractions<T>(fn: () => T | Promise<T>): Promise<T> {
|
|
405
|
+
return new Promise((resolve) => {
|
|
406
|
+
InteractionManager.runAfterInteractions(async () => {
|
|
407
|
+
const result = await fn();
|
|
408
|
+
resolve(result);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Create a debounced function with performance tracking
|
|
415
|
+
*/
|
|
416
|
+
export function createTrackedDebounce<
|
|
417
|
+
T extends (...args: unknown[]) => unknown,
|
|
418
|
+
>(fn: T, delay: number, name: string): T {
|
|
419
|
+
let timeoutId: NodeJS.Timeout;
|
|
420
|
+
let callCount = 0;
|
|
421
|
+
|
|
422
|
+
return ((...args: Parameters<T>) => {
|
|
423
|
+
callCount++;
|
|
424
|
+
clearTimeout(timeoutId);
|
|
425
|
+
|
|
426
|
+
timeoutId = setTimeout(() => {
|
|
427
|
+
if (IS_DEV) {
|
|
428
|
+
console.log(`[Performance] ${name}: Debounced ${callCount} calls to 1`);
|
|
429
|
+
}
|
|
430
|
+
callCount = 0;
|
|
431
|
+
fn(...args);
|
|
432
|
+
}, delay);
|
|
433
|
+
}) as T;
|
|
434
|
+
}
|