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>
233 lines
6.4 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
};
|