Files
maternal-app/maternal-web/lib/performance/monitoring.ts
andupetcu 0a2e28b5ee Implement Phase 7 Performance Optimization and fix tracking system
Phase 7 Implementation:
- Add lazy loading for AI Assistant and Insights pages
- Create LoadingFallback component with skeleton screens (page, card, list, chart, chat variants)
- Create OptimizedImage component with Next.js Image optimization
- Create PerformanceMonitor component with web-vitals v5 integration
- Add performance monitoring library tracking Core Web Vitals (CLS, INP, FCP, LCP, TTFB)
- Install web-vitals v5.1.0 dependency
- Extract AI chat interface and insights dashboard to lazy-loaded components

Tracking System Fixes:
- Fix API data transformation between frontend (timestamp/data) and backend (startedAt/metadata)
- Update createActivity, getActivities, and getActivity to properly transform data structures
- Fix diaper, feeding, and sleep tracking pages to work with backend API

Homepage Improvements:
- Connect Today's Summary to backend daily summary API
- Load real-time data for feeding count, sleep hours, and diaper count
- Add loading states and empty states for better UX
- Format sleep duration as "Xh Ym" for better readability
- Display child name in summary section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-01 09:40:21 +03:00

233 lines
6.4 KiB
TypeScript

import { onCLS, onINP, onFCP, onLCP, onTTFB, type Metric } from 'web-vitals';
/**
* Performance Monitoring Module
*
* Tracks Core Web Vitals metrics:
* - CLS (Cumulative Layout Shift): Measures visual stability
* - INP (Interaction to Next Paint): Measures interactivity (replaces FID in v5)
* - FCP (First Contentful Paint): Measures perceived load speed
* - LCP (Largest Contentful Paint): Measures loading performance
* - TTFB (Time to First Byte): Measures server response time
*
* Sends metrics to analytics (Google Analytics if available)
*/
interface PerformanceMetric {
name: string;
value: number;
id: string;
delta: number;
rating: 'good' | 'needs-improvement' | 'poor';
}
/**
* Send metric to analytics service
*/
const sendToAnalytics = (metric: Metric) => {
const body: PerformanceMetric = {
name: metric.name,
value: Math.round(metric.value),
id: metric.id,
delta: metric.delta,
rating: metric.rating,
};
// Send to Google Analytics if available
if (typeof window !== 'undefined' && (window as any).gtag) {
(window as any).gtag('event', metric.name, {
event_category: 'Web Vitals',
event_label: metric.id,
value: Math.round(metric.value),
metric_id: metric.id,
metric_value: metric.value,
metric_delta: metric.delta,
metric_rating: metric.rating,
non_interaction: true,
});
}
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.log('[Performance]', body);
}
// Send to custom analytics endpoint if needed
if (process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) {
const analyticsEndpoint = process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT;
// Use navigator.sendBeacon for reliable analytics even during page unload
if (navigator.sendBeacon) {
navigator.sendBeacon(
analyticsEndpoint,
JSON.stringify(body)
);
} else {
// Fallback to fetch
fetch(analyticsEndpoint, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
keepalive: true,
}).catch((error) => {
console.error('Failed to send analytics:', error);
});
}
}
};
/**
* Initialize performance monitoring
* Call this function once when the app loads
*/
export const initPerformanceMonitoring = () => {
if (typeof window === 'undefined') {
return;
}
try {
// Track Cumulative Layout Shift (CLS)
// Good: < 0.1, Needs Improvement: < 0.25, Poor: >= 0.25
onCLS(sendToAnalytics);
// Track Interaction to Next Paint (INP) - replaces FID in web-vitals v5
// Good: < 200ms, Needs Improvement: < 500ms, Poor: >= 500ms
onINP(sendToAnalytics);
// Track First Contentful Paint (FCP)
// Good: < 1.8s, Needs Improvement: < 3s, Poor: >= 3s
onFCP(sendToAnalytics);
// Track Largest Contentful Paint (LCP)
// Good: < 2.5s, Needs Improvement: < 4s, Poor: >= 4s
onLCP(sendToAnalytics);
// Track Time to First Byte (TTFB)
// Good: < 800ms, Needs Improvement: < 1800ms, Poor: >= 1800ms
onTTFB(sendToAnalytics);
console.log('[Performance] Monitoring initialized');
} catch (error) {
console.error('[Performance] Failed to initialize monitoring:', error);
}
};
/**
* Report custom performance metrics
*/
export const reportCustomMetric = (name: string, value: number, metadata?: Record<string, any>) => {
if (typeof window !== 'undefined' && (window as any).gtag) {
(window as any).gtag('event', name, {
event_category: 'Custom Metrics',
value: Math.round(value),
...metadata,
});
}
if (process.env.NODE_ENV === 'development') {
console.log('[Performance Custom Metric]', { name, value, metadata });
}
};
/**
* Measure and report component render time
*/
export const measureComponentRender = (componentName: string) => {
if (typeof window === 'undefined' || !window.performance) {
return () => {};
}
const startMark = `${componentName}-render-start`;
const endMark = `${componentName}-render-end`;
const measureName = `${componentName}-render`;
performance.mark(startMark);
return () => {
performance.mark(endMark);
performance.measure(measureName, startMark, endMark);
const measure = performance.getEntriesByName(measureName)[0];
if (measure) {
reportCustomMetric(`component_render_${componentName}`, measure.duration, {
component: componentName,
});
}
// Clean up marks and measures
performance.clearMarks(startMark);
performance.clearMarks(endMark);
performance.clearMeasures(measureName);
};
};
/**
* Track page load time
*/
export const trackPageLoad = (pageName: string) => {
if (typeof window === 'undefined' || !window.performance) {
return;
}
// Wait for load event
window.addEventListener('load', () => {
setTimeout(() => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
if (navigation) {
reportCustomMetric(`page_load_${pageName}`, navigation.loadEventEnd - navigation.fetchStart, {
page: pageName,
dom_content_loaded: navigation.domContentLoadedEventEnd - navigation.fetchStart,
dom_interactive: navigation.domInteractive - navigation.fetchStart,
});
}
}, 0);
});
};
/**
* Monitor long tasks (tasks that block the main thread for > 50ms)
*/
export const monitorLongTasks = () => {
if (typeof window === 'undefined' || !(window as any).PerformanceObserver) {
return;
}
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
reportCustomMetric('long_task', entry.duration, {
start_time: entry.startTime,
duration: entry.duration,
});
}
});
observer.observe({ entryTypes: ['longtask'] });
} catch (error) {
console.error('[Performance] Failed to monitor long tasks:', error);
}
};
/**
* Track resource loading times
*/
export const trackResourceTiming = () => {
if (typeof window === 'undefined' || !window.performance) {
return;
}
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
const slowResources = resources.filter((resource) => resource.duration > 1000);
slowResources.forEach((resource) => {
reportCustomMetric('slow_resource', resource.duration, {
url: resource.name,
type: resource.initiatorType,
size: resource.transferSize,
});
});
};