š
Advanced
14 min readPerformance Monitoring & Profiling
Using Performance API, profilers, and monitoring tools
Understanding Performance Monitoring
Performance monitoring helps identify bottlenecks, track metrics, and ensure your application runs smoothly. Modern browsers provide powerful APIs for measuring and analyzing performance.
Performance API Basics
// Navigation Timing - Page load metrics
const perfData = performance.timing;
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
const domReadyTime = perfData.domContentLoadedEventEnd - perfData.navigationStart;
const dnsTime = perfData.domainLookupEnd - perfData.domainLookupStart;
const tcpTime = perfData.connectEnd - perfData.connectStart;
console.log(`Page Load: ${pageLoadTime}ms`);
console.log(`DOM Ready: ${domReadyTime}ms`);
console.log(`DNS Lookup: ${dnsTime}ms`);
console.log(`TCP Connection: ${tcpTime}ms`);
// Modern Navigation Timing API (Level 2)
const [navigationEntry] = performance.getEntriesByType('navigation');
console.log('DNS:', navigationEntry.domainLookupEnd - navigationEntry.domainLookupStart);
console.log('TCP:', navigationEntry.connectEnd - navigationEntry.connectStart);
console.log('DOM Interactive:', navigationEntry.domInteractive);
console.log('DOM Complete:', navigationEntry.domComplete);
Performance Marks and Measures
// Mark specific points in time
performance.mark('start-data-fetch');
// Simulate data fetching
await fetchData();
performance.mark('end-data-fetch');
// Measure duration between marks
performance.measure(
'data-fetch-duration',
'start-data-fetch',
'end-data-fetch'
);
// Get measurement
const measure = performance.getEntriesByName('data-fetch-duration')[0];
console.log(`Data fetch took ${measure.duration}ms`);
// Comprehensive example
class PerformanceTracker {
mark(name) {
performance.mark(name);
}
measure(measureName, startMark, endMark) {
performance.measure(measureName, startMark, endMark);
const entries = performance.getEntriesByName(measureName);
return entries[entries.length - 1];
}
async trackAsync(name, fn) {
this.mark(`${name}-start`);
try {
const result = await fn();
this.mark(`${name}-end`);
const measure = this.measure(name, `${name}-start`, `${name}-end`);
console.log(`${name}: ${measure.duration.toFixed(2)}ms`);
return result;
} catch (error) {
console.error(`${name} failed:`, error);
throw error;
}
}
clear() {
performance.clearMarks();
performance.clearMeasures();
}
}
// Usage
const tracker = new PerformanceTracker();
await tracker.trackAsync('api-call', async () => {
return fetch('/api/data').then(r => r.json());
});
Core Web Vitals Monitoring
// Install web-vitals library
// npm install web-vitals
import { getLCP, getFID, getCLS, getFCP, getTTFB } from 'web-vitals';
// Largest Contentful Paint (LCP) - should be < 2.5s
getLCP((metric) => {
console.log('LCP:', metric.value);
sendToAnalytics('LCP', metric.value);
});
// First Input Delay (FID) - should be < 100ms
getFID((metric) => {
console.log('FID:', metric.value);
sendToAnalytics('FID', metric.value);
});
// Cumulative Layout Shift (CLS) - should be < 0.1
getCLS((metric) => {
console.log('CLS:', metric.value);
sendToAnalytics('CLS', metric.value);
});
// First Contentful Paint (FCP) - should be < 1.8s
getFCP((metric) => {
console.log('FCP:', metric.value);
sendToAnalytics('FCP', metric.value);
});
// Time to First Byte (TTFB) - should be < 600ms
getTTFB((metric) => {
console.log('TTFB:', metric.value);
sendToAnalytics('TTFB', metric.value);
});
// Send to analytics service
function sendToAnalytics(metricName, value) {
// Google Analytics example
if (window.gtag) {
gtag('event', metricName, {
value: Math.round(value),
metric_id: metricName,
metric_value: value,
metric_delta: 0
});
}
}
Resource Timing API
// Monitor resource loading performance
function analyzeResources() {
const resources = performance.getEntriesByType('resource');
// Group by resource type
const byType = resources.reduce((acc, resource) => {
const type = resource.initiatorType;
if (!acc[type]) acc[type] = [];
acc[type].push(resource);
return acc;
}, {});
// Analyze each type
Object.entries(byType).forEach(([type, items]) => {
const totalSize = items.reduce((sum, item) => sum + (item.transferSize || 0), 0);
const avgDuration = items.reduce((sum, item) => sum + item.duration, 0) / items.length;
console.log(`${type}:`);
console.log(` Count: ${items.length}`);
console.log(` Total Size: ${(totalSize / 1024).toFixed(2)} KB`);
console.log(` Avg Duration: ${avgDuration.toFixed(2)}ms`);
});
// Find slow resources
const slowResources = resources
.filter(r => r.duration > 1000)
.sort((a, b) => b.duration - a.duration);
console.log('Slow resources (>1s):');
slowResources.forEach(r => {
console.log(` ${r.name}: ${r.duration.toFixed(2)}ms`);
});
}
// Run analysis after page load
window.addEventListener('load', () => {
setTimeout(analyzeResources, 1000);
});
Performance Observer
// Monitor performance entries in real-time
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.entryType}: ${entry.name}`, entry);
// Log slow tasks (> 50ms)
if (entry.entryType === 'measure' && entry.duration > 50) {
console.warn(`Slow operation: ${entry.name} took ${entry.duration}ms`);
}
}
});
// Observe specific entry types
observer.observe({
entryTypes: ['measure', 'navigation', 'resource', 'paint']
});
// Long Task API - detect tasks blocking main thread
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long task detected:', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name
});
// Send to monitoring service
reportLongTask(entry);
}
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
React Profiler
import { Profiler } from 'react';
// Measure component render performance
function onRenderCallback(
id, // Component ID
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime, // When render started
commitTime, // When changes committed
interactions // Set of interactions
) {
console.log(`${id} (${phase}): ${actualDuration}ms`);
// Send to analytics
if (actualDuration > 16) { // Longer than one frame at 60fps
console.warn(`Slow render in ${id}`);
}
}
function App() {
return (
);
}
// Nest profilers for granular measurement
function Dashboard() {
return (
);
}
Custom Performance Monitoring Hook
// React hook for performance monitoring
function usePerformance(componentName) {
const renderCount = useRef(0);
const renderTimes = useRef([]);
useEffect(() => {
renderCount.current++;
const startTime = performance.now();
return () => {
const endTime = performance.now();
const duration = endTime - startTime;
renderTimes.current.push(duration);
// Keep last 10 renders
if (renderTimes.current.length > 10) {
renderTimes.current.shift();
}
const avgTime = renderTimes.current.reduce((a, b) => a + b, 0) / renderTimes.current.length;
console.log(`${componentName}:`);
console.log(` Renders: ${renderCount.current}`);
console.log(` Last render: ${duration.toFixed(2)}ms`);
console.log(` Avg render: ${avgTime.toFixed(2)}ms`);
};
});
}
// Usage
function HeavyComponent() {
usePerformance('HeavyComponent');
// Component logic
return ...;
}
Browser DevTools Profiling
// Programmatically start/stop profiling
console.profile('MyOperation');
// Code to profile
performExpensiveOperation();
console.profileEnd('MyOperation');
// Measure memory usage
console.memory; // { usedJSHeapSize, totalJSHeapSize, jsHeapSizeLimit }
// Take heap snapshot programmatically
if (window.performance && performance.memory) {
const before = performance.memory.usedJSHeapSize;
// Code that might leak memory
createManyObjects();
const after = performance.memory.usedJSHeapSize;
const delta = after - before;
console.log(`Memory delta: ${(delta / 1024 / 1024).toFixed(2)} MB`);
}
// Performance timeline
performance.getEntries().forEach(entry => {
console.log(entry.name, entry.duration);
});
Real User Monitoring (RUM)
// Collect and send performance data
class RUMMonitor {
constructor(endpoint) {
this.endpoint = endpoint;
this.metrics = {};
}
collectMetrics() {
// Navigation timing
const navigation = performance.getEntriesByType('navigation')[0];
this.metrics = {
// Page load metrics
pageLoad: navigation.loadEventEnd - navigation.fetchStart,
domLoad: navigation.domContentLoadedEventEnd - navigation.fetchStart,
// Network metrics
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp: navigation.connectEnd - navigation.connectStart,
ttfb: navigation.responseStart - navigation.requestStart,
download: navigation.responseEnd - navigation.responseStart,
// Resource counts
resources: performance.getEntriesByType('resource').length,
// Browser info
userAgent: navigator.userAgent,
connection: navigator.connection?.effectiveType,
// Timestamp
timestamp: Date.now()
};
}
async send() {
this.collectMetrics();
try {
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.metrics),
keepalive: true // Ensure request completes even if page unloads
});
} catch (error) {
console.error('Failed to send metrics:', error);
}
}
}
// Initialize and send on page load
const monitor = new RUMMonitor('/api/metrics');
window.addEventListener('load', () => {
setTimeout(() => monitor.send(), 1000);
});
// Send on page unload
window.addEventListener('beforeunload', () => {
monitor.send();
});
Best Practices
- Monitor Core Web Vitals (LCP, FID, CLS) for user experience
- Use Performance Observer for real-time monitoring
- Set performance budgets and alert when exceeded
- Track custom metrics with marks and measures
- Profile in production with small sample of users
- Use React Profiler for component-level optimization
- Monitor long tasks that block the main thread (>50ms)
- Implement Real User Monitoring (RUM) for production insights
- Compare performance across different browsers and devices
- Set up automated performance testing in CI/CD pipeline