Before you can optimize, you must measure. Intuition about what is slow is frequently wrong — developers routinely optimize the wrong code path because they guessed instead of profiled. The browser's Performance API provides sub-millisecond timing precision, named markers for annotating your code, and observer patterns for continuous monitoring, all without any external libraries.
This lesson covers the User Timing API for custom measurements, PerformanceObserver for automated monitoring, and the navigation and resource timing APIs for understanding page load performance.
Key Concepts
- performance.now(): Returns a high-resolution timestamp in milliseconds (with microsecond precision) relative to the page's navigation start. Unlike
Date.now(), it is monotonic (never goes backward) and unaffected by system clock adjustments. - performance.mark(): Creates a named timestamp in the performance timeline. Marks serve as reference points that you can measure between.
- performance.measure(): Creates a named duration measurement between two marks (or from navigation start to a mark). The resulting entry includes the duration, start time, and name.
- PerformanceObserver: An API for asynchronously observing performance entries as they are recorded. It can observe marks, measures, long tasks, resource loads, paint events, and Web Vitals metrics.
- User Timing API: The collective name for
performance.mark()andperformance.measure(), providing developers with a standardized way to instrument their code with custom timing data.
Real World Context
Production applications use the Performance API to collect Real User Monitoring (RUM) data. Companies like Google, Netflix, and Airbnb send performance measurements to analytics services to track Core Web Vitals (LCP, FID, CLS) across their user base. The User Timing API integrates directly with Chrome DevTools — marks and measures appear as labeled annotations in the Performance panel's timeline, making it easy to correlate custom measurements with browser activity. Server-side rendering frameworks use navigation timing to measure Time to First Byte (TTFB) and hydration duration.
Deep Dive
The simplest measurement uses performance.now() for high-resolution timestamps:
javascriptconst start = performance.now(); expensiveOperation(); const duration = performance.now() - start; console.log(`Operation took ${duration.toFixed(2)}ms`);
For more structured measurement, use marks and measures:
javascriptperformance.mark('render-start'); renderComponent(); performance.mark('render-end'); performance.measure('render-time', 'render-start', 'render-end'); const [entry] = performance.getEntriesByName('render-time'); console.log(`Render took ${entry.duration.toFixed(2)}ms`);
PerformanceObserver provides a non-blocking way to monitor entries as they are recorded, including buffered entries from before the observer was created:
javascriptconst observer = new PerformanceObserver(list => { for (const entry of list.getEntries()) { if (entry.duration > 50) { console.warn('Slow task:', entry.name, entry.duration); } } }); observer.observe({ type: 'measure', buffered: true });
You can observe multiple entry types for comprehensive monitoring:
javascript// Monitor resource loading new PerformanceObserver(list => { for (const entry of list.getEntries()) { if (entry.duration > 1000) { console.warn(`Slow resource: ${entry.name} (${entry.duration}ms)`); } } }).observe({ type: 'resource', buffered: true }); // Monitor navigation timing new PerformanceObserver(list => { const [nav] = list.getEntries(); console.log('TTFB:', nav.responseStart - nav.requestStart); console.log('DOM Interactive:', nav.domInteractive); console.log('DOM Complete:', nav.domComplete); }).observe({ type: 'navigation', buffered: true });
For production monitoring, send measurements to your analytics endpoint:
javascriptfunction reportMetric(name, value) { navigator.sendBeacon('/analytics', JSON.stringify({ name, value, url: location.href })); } new PerformanceObserver(list => { for (const entry of list.getEntries()) { reportMetric(entry.name, entry.duration); } }).observe({ type: 'measure', buffered: true });
Common Pitfalls
- Using Date.now() for performance measurement:
Date.now()only has millisecond precision, is not monotonic (can jump backward due to clock synchronization), and includes time spent in sleep/hibernation. Always useperformance.now()for timing. - Forgetting the
buffered: trueoption: Withoutbuffered: true, PerformanceObserver only captures entries created after the observer is registered. Entries recorded during page load will be missed. - Not cleaning up observers: In single-page applications, PerformanceObservers created in components should be disconnected when the component unmounts to prevent memory leaks and duplicate reporting.
Best Practices
- Use descriptive mark names that include the component or feature being measured (e.g.,
'dashboard-render-start'rather than'start') so they are identifiable in DevTools. - Always use
buffered: truewhen creating PerformanceObservers to capture entries that were recorded before your observer code ran, especially for page load metrics. - Send performance data using
navigator.sendBeacon()rather thanfetch()to ensure data is transmitted even during page unload.
Summary
The Performance API provides high-resolution timing with performance.now(), structured measurement with marks and measures, and asynchronous monitoring with PerformanceObserver. These tools integrate directly with Chrome DevTools and form the foundation of Real User Monitoring. Use navigator.sendBeacon() to reliably transmit performance data to analytics services, and always prefer performance.now() over Date.now() for timing measurements.
Code Examples
// Mark and measure
performance.mark('render-start');
renderComponent();
performance.mark('render-end');
performance.measure('render-time', 'render-start', 'render-end');
const [entry] = performance.getEntriesByName('render-time');
console.log(`Render took ${entry.duration.toFixed(2)}ms`);
// PerformanceObserver for continuous monitoring
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('Slow task:', entry.name, entry.duration);
}
}
});
observer.observe({ type: 'measure', buffered: true });