Performance and Timing APIs
1. Performance API and Navigation Timing
| Property | Type | Description | Browser Support |
|---|---|---|---|
| performance.now() | number |
Returns high-resolution timestamp in ms since time origin. More accurate than Date.now(). |
All Browsers |
| performance.timeOrigin | number |
Unix timestamp when performance measurement started. Use to convert to absolute time. | All Modern Browsers |
| performance.timing | PerformanceTiming |
Navigation timing info (deprecated, use Navigation Timing Level 2). Legacy support. | All Browsers |
| performance.navigation | PerformanceNavigation |
Navigation type and redirect count (deprecated). Use performance.getEntriesByType("navigation"). |
All Browsers |
| Navigation Timing Metric | Description | Calculation |
|---|---|---|
| DNS Lookup | Time to resolve domain name. | domainLookupEnd - domainLookupStart |
| TCP Connection | Time to establish TCP connection. | connectEnd - connectStart |
| TLS Negotiation | Time for SSL/TLS handshake. | connectEnd - secureConnectionStart |
| Request Time | Time to send request. | responseStart - requestStart |
| Response Time | Time to receive response. | responseEnd - responseStart |
| DOM Processing | Time to parse and process DOM. | domComplete - domLoading |
| DOM Interactive | Time until DOM ready (DOMContentLoaded). | domInteractive - navigationStart |
| DOM Complete | Time until page fully loaded. | domComplete - navigationStart |
| Page Load Time | Total time from navigation to load complete. | loadEventEnd - navigationStart |
Example: Navigation timing metrics
// Get navigation timing (modern way)
const [navigation] = performance.getEntriesByType("navigation");
if (navigation) {
console.log("Navigation type:", navigation.type); // "navigate", "reload", "back_forward", "prerender"
console.log("Redirect count:", navigation.redirectCount);
// Calculate metrics
const metrics = {
"dns": navigation.domainLookupEnd - navigation.domainLookupStart,
"tcp": navigation.connectEnd - navigation.connectStart,
"tls": navigation.secureConnectionStart ?
navigation.connectEnd - navigation.secureConnectionStart : 0,
"ttfb": navigation.responseStart - navigation.requestStart, // Time to First Byte
"responseTime": navigation.responseEnd - navigation.responseStart,
"domParsing": navigation.domInteractive - navigation.responseEnd,
"domComplete": navigation.domComplete - navigation.domLoading,
"pageLoad": navigation.loadEventEnd - navigation.loadEventStart,
"total": navigation.loadEventEnd - navigation.fetchStart
};
console.log("Performance Metrics (ms):");
Object.entries(metrics).forEach(([key, value]) => {
console.log(` ${key}: ${value.toFixed(2)}ms`);
});
}
// High-resolution timestamp
const start = performance.now();
// ... do work ...
const end = performance.now();
console.log(`Execution time: ${(end - start).toFixed(2)}ms`);
// Convert to absolute time
const absoluteTime = performance.timeOrigin + performance.now();
console.log("Absolute timestamp:", new Date(absoluteTime));
// Legacy navigation timing (deprecated but widely supported)
if (performance.timing) {
const timing = performance.timing;
const pageLoadTime = timing.loadEventEnd - timing.navigationStart;
const domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart;
console.log(`Page load: ${pageLoadTime}ms`);
console.log(`DOM ready: ${domReadyTime}ms`);
}
Note: Use performance.now() instead of
Date.now() for accurate timing - higher resolution and not affected by system clock changes. Navigation Timing Level 1 (performance.timing) is deprecated but still widely supported. Use getEntriesByType("navigation") for modern apps.
2. Resource Timing for Asset Loading Analysis
| Method | Description | Returns |
|---|---|---|
| performance.getEntriesByType("resource") | Gets all resource timing entries (CSS, JS, images, etc.). | PerformanceResourceTiming[] |
| performance.getEntriesByName(name, "resource") | Gets resource entries for specific URL. | PerformanceResourceTiming[] |
| performance.clearResourceTimings() | Clears resource timing buffer. | void |
| performance.setResourceTimingBufferSize(n) | Sets max number of resource entries (default 250). | void |
| PerformanceResourceTiming Property | Type | Description |
|---|---|---|
| name | string |
Resource URL. |
| initiatorType | string |
How resource loaded: "script", "link", "img", "fetch", "xmlhttprequest", etc. |
| duration | number |
Total load time in ms (responseEnd - startTime). |
| transferSize | number |
Size of response including headers (bytes). 0 if cached. |
| encodedBodySize | number |
Compressed size of response body (bytes). |
| decodedBodySize | number |
Uncompressed size of response body (bytes). |
| nextHopProtocol | string |
Network protocol: "http/1.1", "h2", "h3", etc. |
Example: Analyze resource loading
// Get all resources
const resources = performance.getEntriesByType("resource");
console.log(`Total resources: ${resources.length}`);
// Group by type
const byType = resources.reduce((acc, resource) => {
const type = resource.initiatorType;
acc[type] = acc[type] || [];
acc[type].push(resource);
return acc;
}, {});
// Analyze each type
Object.entries(byType).forEach(([type, items]) => {
const totalSize = items.reduce((sum, r) => sum + r.transferSize, 0);
const totalTime = items.reduce((sum, r) => sum + r.duration, 0);
console.log(`\n${type}:`);
console.log(` Count: ${items.length}`);
console.log(` Total size: ${(totalSize / 1024).toFixed(2)} KB`);
console.log(` Avg time: ${(totalTime / items.length).toFixed(2)}ms`);
});
// Find slow resources
const slowResources = resources
.filter(r => r.duration > 1000) // > 1 second
.sort((a, b) => b.duration - a.duration);
console.log("\nSlow resources (>1s):");
slowResources.forEach(r => {
console.log(` ${r.name}: ${r.duration.toFixed(2)}ms (${(r.transferSize / 1024).toFixed(2)} KB)`);
});
// Check cache hits
const cached = resources.filter(r => r.transferSize === 0);
console.log(`\nCached resources: ${cached.length} / ${resources.length}`);
// Check compression
resources.forEach(r => {
if (r.encodedBodySize > 0 && r.decodedBodySize > 0) {
const ratio = (1 - r.encodedBodySize / r.decodedBodySize) * 100;
if (ratio > 50) {
console.log(`Well compressed (${ratio.toFixed(0)}%): ${r.name}`);
} else if (ratio < 10 && r.decodedBodySize > 10000) {
console.log(`Poor compression (${ratio.toFixed(0)}%): ${r.name}`);
}
}
});
// Check HTTP/2
const http2Resources = resources.filter(r => r.nextHopProtocol === "h2");
console.log(`\nHTTP/2 resources: ${http2Resources.length} / ${resources.length}`);
// Timing breakdown for specific resource
const scriptUrl = "/app.js";
const [script] = performance.getEntriesByName(scriptUrl, "resource");
if (script) {
console.log(`\nTiming for ${scriptUrl}:`);
console.log(` DNS: ${(script.domainLookupEnd - script.domainLookupStart).toFixed(2)}ms`);
console.log(` TCP: ${(script.connectEnd - script.connectStart).toFixed(2)}ms`);
console.log(` Request: ${(script.responseStart - script.requestStart).toFixed(2)}ms`);
console.log(` Response: ${(script.responseEnd - script.responseStart).toFixed(2)}ms`);
console.log(` Total: ${script.duration.toFixed(2)}ms`);
}
Note:
transferSize is 0 for cached resources. For cross-origin resources, need Timing-Allow-Origin header to get detailed timing. Resource buffer has default limit of 250 entries - increase with setResourceTimingBufferSize() if needed.
Warning: Resource timing can consume memory with many resources. Clear periodically with
clearResourceTimings() in SPAs. Cross-origin resources show limited timing without CORS headers. Don't rely on exact values - use for relative comparisons and trends.
3. User Timing API for Custom Metrics
| Method | Description | Use Case |
|---|---|---|
| performance.mark(name, options) | Creates named timestamp marker. Options: startTime, detail. |
Mark important events |
| performance.measure(name, options) | Measures duration between marks/events. Options: start, end, duration. |
Measure custom metrics |
| performance.clearMarks(name) | Clears specific or all marks. Omit name to clear all. | Cleanup |
| performance.clearMeasures(name) | Clears specific or all measures. Omit name to clear all. | Cleanup |
| performance.getEntriesByType("mark") | Gets all marks. Returns PerformanceMark[]. |
Retrieve marks |
| performance.getEntriesByType("measure") | Gets all measures. Returns PerformanceMeasure[]. |
Retrieve measures |
Example: User timing for custom metrics
// Mark events
performance.mark("page-start");
// Simulate work
await loadData();
performance.mark("data-loaded");
await renderUI();
performance.mark("ui-rendered");
await initializeFeatures();
performance.mark("features-ready");
// Measure durations
performance.measure("data-load-time", "page-start", "data-loaded");
performance.measure("ui-render-time", "data-loaded", "ui-rendered");
performance.measure("feature-init-time", "ui-rendered", "features-ready");
performance.measure("total-init-time", "page-start", "features-ready");
// Get measurements
const measures = performance.getEntriesByType("measure");
console.log("Custom Metrics:");
measures.forEach(measure => {
console.log(` ${measure.name}: ${measure.duration.toFixed(2)}ms`);
});
// Measure with navigation timing reference
performance.measure("time-to-interactive", {
"start": 0, // Navigation start
"end": "features-ready"
});
// Measure with explicit duration
performance.measure("render-budget", {
"start": "data-loaded",
"duration": 16.67 // 60fps budget
});
// Add custom data to marks
performance.mark("api-call", {
"detail": {
"endpoint": "/api/users",
"method": "GET",
"status": 200
}
});
// Retrieve with detail
const marks = performance.getEntriesByName("api-call", "mark");
console.log("API call detail:", marks[0].detail);
// Clear old marks/measures
performance.clearMarks("page-start");
performance.clearMeasures("data-load-time");
// Clear all
performance.clearMarks();
performance.clearMeasures();
Example: Performance monitoring helper
class PerformanceMonitor {
constructor() {
this.metrics = new Map();
}
// Start timing
start(name) {
const markName = `${name}-start`;
performance.mark(markName);
this.metrics.set(name, { "startMark": markName });
}
// End timing and measure
end(name) {
const markName = `${name}-end`;
performance.mark(markName);
const metric = this.metrics.get(name);
if (metric) {
performance.measure(name, metric.startMark, markName);
const [measure] = performance.getEntriesByName(name, "measure");
metric.duration = measure.duration;
console.log(`${name}: ${measure.duration.toFixed(2)}ms`);
return measure.duration;
}
}
// Get metric
get(name) {
return this.metrics.get(name);
}
// Get all metrics
getAll() {
return Object.fromEntries(this.metrics);
}
// Report to analytics
report() {
const measures = performance.getEntriesByType("measure");
measures.forEach(measure => {
// Send to analytics service
sendAnalytics({
"metric": measure.name,
"value": measure.duration,
"timestamp": Date.now()
});
});
}
// Clear all
clear() {
performance.clearMarks();
performance.clearMeasures();
this.metrics.clear();
}
}
// Usage
const monitor = new PerformanceMonitor();
monitor.start("database-query");
const data = await fetchFromDatabase();
monitor.end("database-query");
monitor.start("render-component");
renderComponent(data);
monitor.end("render-component");
console.log("All metrics:", monitor.getAll());
monitor.report();
Note: User Timing API is perfect for custom metrics specific to your app. Integrates with browser DevTools performance timeline. Use descriptive names for marks and measures. Can add custom data with
detail option.
4. Paint Timing and Largest Contentful Paint
| Paint Metric | Description | Good Value |
|---|---|---|
| First Paint (FP) | Time when browser first renders anything (background, border, etc.). | < 1s |
| First Contentful Paint (FCP) | Time when first text/image renders. Core Web Vital. | < 1.8s |
| Largest Contentful Paint (LCP) | Time when largest content element renders. Core Web Vital. | < 2.5s |
| First Input Delay (FID) | Time from first interaction to browser response. Core Web Vital. | < 100ms |
| Cumulative Layout Shift (CLS) | Visual stability - sum of unexpected layout shifts. Core Web Vital. | < 0.1 |
Example: Measure paint timing
// Get paint timing
const paintEntries = performance.getEntriesByType("paint");
paintEntries.forEach(entry => {
console.log(`${entry.name}: ${entry.startTime.toFixed(2)}ms`);
});
// First Paint and First Contentful Paint
const fp = paintEntries.find(e => e.name === "first-paint");
const fcp = paintEntries.find(e => e.name === "first-contentful-paint");
if (fp) console.log(`FP: ${fp.startTime.toFixed(2)}ms`);
if (fcp) console.log(`FCP: ${fcp.startTime.toFixed(2)}ms`);
// Largest Contentful Paint (LCP)
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log(`LCP: ${lastEntry.startTime.toFixed(2)}ms`);
console.log("LCP element:", lastEntry.element);
console.log("LCP size:", lastEntry.size);
console.log("LCP URL:", lastEntry.url);
});
observer.observe({ "type": "largest-contentful-paint", "buffered": true });
// First Input Delay (FID)
const fidObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
const fid = entry.processingStart - entry.startTime;
console.log(`FID: ${fid.toFixed(2)}ms`);
console.log("Input type:", entry.name);
});
});
fidObserver.observe({ "type": "first-input", "buffered": true });
// Cumulative Layout Shift (CLS)
let clsScore = 0;
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Only count layout shifts without recent user input
if (!entry.hadRecentInput) {
clsScore += entry.value;
console.log(`Layout shift: ${entry.value.toFixed(4)}`);
console.log("Shifted elements:", entry.sources);
}
}
console.log(`Total CLS: ${clsScore.toFixed(4)}`);
});
clsObserver.observe({ "type": "layout-shift", "buffered": true });
Example: Web Vitals monitoring
class WebVitalsMonitor {
constructor() {
this.vitals = {};
this.observers = [];
}
// Measure LCP
measureLCP(callback) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.vitals.lcp = {
"value": lastEntry.startTime,
"rating": this.getRating(lastEntry.startTime, [2500, 4000]),
"element": lastEntry.element,
"url": lastEntry.url
};
callback && callback(this.vitals.lcp);
});
observer.observe({ "type": "largest-contentful-paint", "buffered": true });
this.observers.push(observer);
}
// Measure FID
measureFID(callback) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
const fid = entry.processingStart - entry.startTime;
this.vitals.fid = {
"value": fid,
"rating": this.getRating(fid, [100, 300]),
"eventType": entry.name
};
callback && callback(this.vitals.fid);
});
});
observer.observe({ "type": "first-input", "buffered": true });
this.observers.push(observer);
}
// Measure CLS
measureCLS(callback) {
let clsScore = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
this.vitals.cls = {
"value": clsScore,
"rating": this.getRating(clsScore, [0.1, 0.25])
};
callback && callback(this.vitals.cls);
});
observer.observe({ "type": "layout-shift", "buffered": true });
this.observers.push(observer);
}
// Get rating (good, needs-improvement, poor)
getRating(value, [good, poor]) {
if (value <= good) return "good";
if (value <= poor) return "needs-improvement";
return "poor";
}
// Report all vitals
report() {
console.log("Web Vitals:", this.vitals);
// Send to analytics
Object.entries(this.vitals).forEach(([name, data]) => {
sendAnalytics({
"metric": name.toUpperCase(),
"value": data.value,
"rating": data.rating
});
});
}
// Disconnect all observers
disconnect() {
this.observers.forEach(o => o.disconnect());
}
}
// Usage
const vitalsMonitor = new WebVitalsMonitor();
vitalsMonitor.measureLCP((lcp) => {
console.log(`LCP: ${lcp.value.toFixed(2)}ms (${lcp.rating})`);
});
vitalsMonitor.measureFID((fid) => {
console.log(`FID: ${fid.value.toFixed(2)}ms (${fid.rating})`);
});
vitalsMonitor.measureCLS((cls) => {
console.log(`CLS: ${cls.value.toFixed(4)} (${cls.rating})`);
});
// Report on page unload
window.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
vitalsMonitor.report();
}
});
Note: Core Web Vitals (LCP, FID, CLS) are Google ranking factors. Use PerformanceObserver with
buffered: true to get historical entries. LCP can change as page loads - track latest value. Consider using web-vitals library for production.
Warning: Paint timing varies greatly by device and network. Don't compare absolute values across users - track trends. LCP element must be visible in viewport. CLS excludes shifts within 500ms of user input. Test on real devices, not just desktop.
5. Long Tasks API for Performance Monitoring
| Property | Description | Browser Support |
|---|---|---|
| Long Task | Task that blocks main thread for >50ms. Causes jank and poor responsiveness. | Chrome, Edge |
| entry.duration | Task duration in ms. Long tasks are >50ms. | Chrome, Edge |
| entry.attribution | Array of task attribution (container, script URL, etc.). | Chrome, Edge |
Example: Monitor long tasks
// Observe long tasks (>50ms)
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
console.warn(`Long task detected: ${entry.duration.toFixed(2)}ms`);
console.log("Start time:", entry.startTime);
console.log("Attribution:", entry.attribution);
// Identify source
if (entry.attribution && entry.attribution.length > 0) {
const attribution = entry.attribution[0];
console.log("Container:", attribution.containerType);
console.log("Name:", attribution.containerName);
console.log("Source:", attribution.containerSrc);
}
// Track for analytics
sendAnalytics({
"event": "long-task",
"duration": entry.duration,
"timestamp": entry.startTime
});
});
});
// Check support
if (PerformanceObserver.supportedEntryTypes.includes("longtask")) {
observer.observe({ "type": "longtask", "buffered": true });
} else {
console.log("Long Tasks API not supported");
}
// Track total long task time
let totalBlockingTime = 0;
const tbtObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Only count blocking portion (>50ms)
const blockingTime = Math.max(0, entry.duration - 50);
totalBlockingTime += blockingTime;
}
console.log(`Total Blocking Time: ${totalBlockingTime.toFixed(2)}ms`);
});
if (PerformanceObserver.supportedEntryTypes.includes("longtask")) {
tbtObserver.observe({ "type": "longtask", "buffered": true });
}
Note: Long Tasks API has limited browser support (Chrome, Edge). Long tasks (>50ms) cause poor interactivity. Total Blocking Time (TBT) is sum of blocking portions of all long tasks. Use to identify performance bottlenecks.
6. Performance Observer for Metrics Collection
| Method | Description |
|---|---|
| new PerformanceObserver(callback) | Creates observer. Callback receives PerformanceObserverEntryList. |
| observer.observe(options) | Start observing. Options: type, entryTypes, buffered. |
| observer.disconnect() | Stop observing and clear buffer. |
| observer.takeRecords() | Returns and clears buffered entries. |
| PerformanceObserver.supportedEntryTypes | Static property - array of supported entry types in browser. |
| Entry Type | Description | Browser Support |
|---|---|---|
| navigation | Navigation timing. | All Modern |
| resource | Resource timing. | All Modern |
| mark | User timing marks. | All Modern |
| measure | User timing measures. | All Modern |
| paint | Paint timing (FP, FCP). | All Modern |
| largest-contentful-paint | LCP timing. | Chrome, Edge |
| first-input | FID timing. | Chrome, Edge |
| layout-shift | CLS events. | Chrome, Edge |
| longtask | Long tasks (>50ms). | Chrome, Edge |
| element | Element timing. | Limited |
Example: Performance observer patterns
// Observe single type
const paintObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.startTime.toFixed(2)}ms`);
}
});
paintObserver.observe({ "type": "paint", "buffered": true });
// Observe multiple types (legacy way)
const multiObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`[${entry.entryType}] ${entry.name}: ${entry.duration || entry.startTime}ms`);
}
});
multiObserver.observe({
"entryTypes": ["mark", "measure", "resource"]
});
// Check supported types
console.log("Supported types:", PerformanceObserver.supportedEntryTypes);
// Feature detection
function observeIfSupported(type, callback) {
if (PerformanceObserver.supportedEntryTypes.includes(type)) {
const observer = new PerformanceObserver(callback);
observer.observe({ "type": type, "buffered": true });
return observer;
} else {
console.warn(`${type} not supported`);
return null;
}
}
// Usage
const lcpObserver = observeIfSupported("largest-contentful-paint", (list) => {
const entries = list.getEntries();
const lcp = entries[entries.length - 1];
console.log(`LCP: ${lcp.startTime.toFixed(2)}ms`);
});
// Buffered option gets historical entries
const resourceObserver = new PerformanceObserver((list) => {
console.log(`New resources loaded: ${list.getEntries().length}`);
});
// buffered: true includes entries before observe() called
resourceObserver.observe({ "type": "resource", "buffered": true });
// Disconnect observer
setTimeout(() => {
resourceObserver.disconnect();
console.log("Observer disconnected");
}, 10000);
// Take records manually
const manualObserver = new PerformanceObserver(() => {});
manualObserver.observe({ "type": "mark" });
performance.mark("test-1");
performance.mark("test-2");
const records = manualObserver.takeRecords();
console.log("Manual records:", records);
manualObserver.disconnect();
Example: Comprehensive performance monitoring
class PerformanceTracker {
constructor() {
this.observers = [];
this.metrics = {};
}
// Initialize all observers
init() {
this.observeNavigation();
this.observeResources();
this.observePaint();
this.observeWebVitals();
this.observeLongTasks();
}
observeNavigation() {
this.observe("navigation", (list) => {
const [nav] = list.getEntries();
this.metrics.navigation = {
"type": nav.type,
"redirectCount": nav.redirectCount,
"dns": nav.domainLookupEnd - nav.domainLookupStart,
"tcp": nav.connectEnd - nav.connectStart,
"ttfb": nav.responseStart - nav.requestStart,
"domInteractive": nav.domInteractive,
"domComplete": nav.domComplete,
"loadComplete": nav.loadEventEnd
};
});
}
observeResources() {
this.observe("resource", (list) => {
const resources = list.getEntries();
if (!this.metrics.resources) {
this.metrics.resources = { "count": 0, "totalSize": 0, "byType": {} };
}
resources.forEach(r => {
this.metrics.resources.count++;
this.metrics.resources.totalSize += r.transferSize;
const type = r.initiatorType;
if (!this.metrics.resources.byType[type]) {
this.metrics.resources.byType[type] = { "count": 0, "size": 0 };
}
this.metrics.resources.byType[type].count++;
this.metrics.resources.byType[type].size += r.transferSize;
});
});
}
observePaint() {
this.observe("paint", (list) => {
for (const entry of list.getEntries()) {
this.metrics[entry.name] = entry.startTime;
}
});
}
observeWebVitals() {
// LCP
this.observe("largest-contentful-paint", (list) => {
const entries = list.getEntries();
const lcp = entries[entries.length - 1];
this.metrics.lcp = lcp.startTime;
});
// FID
this.observe("first-input", (list) => {
const [entry] = list.getEntries();
this.metrics.fid = entry.processingStart - entry.startTime;
});
// CLS
let clsScore = 0;
this.observe("layout-shift", (list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
this.metrics.cls = clsScore;
});
}
observeLongTasks() {
this.observe("longtask", (list) => {
if (!this.metrics.longTasks) {
this.metrics.longTasks = { "count": 0, "totalTime": 0 };
}
for (const entry of list.getEntries()) {
this.metrics.longTasks.count++;
this.metrics.longTasks.totalTime += entry.duration;
}
});
}
// Helper to observe with feature detection
observe(type, callback) {
if (PerformanceObserver.supportedEntryTypes.includes(type)) {
const observer = new PerformanceObserver(callback);
observer.observe({ "type": type, "buffered": true });
this.observers.push(observer);
}
}
// Get all metrics
getMetrics() {
return this.metrics;
}
// Report to analytics
report() {
console.log("Performance Metrics:", this.metrics);
// Send to analytics service
sendAnalytics({
"event": "performance-report",
"metrics": this.metrics,
"timestamp": Date.now()
});
}
// Cleanup
disconnect() {
this.observers.forEach(o => o.disconnect());
}
}
// Initialize tracker
const tracker = new PerformanceTracker();
tracker.init();
// Report on visibility change
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
tracker.report();
}
});
// Report on page unload
window.addEventListener("pagehide", () => {
tracker.report();
});
Note: Always use feature detection with
PerformanceObserver.supportedEntryTypes. Use buffered: true to get entries that occurred before observe() called. Modern approach uses type (single type), legacy uses entryTypes (array).
Warning: Performance observers can impact performance if callback is expensive. Keep callbacks lightweight. Disconnect observers when done to prevent memory leaks. Don't observe in tight loops. Use debouncing for resource observer in SPAs.
Performance API Best Practices
- Use
performance.now()for accurate timing - higher resolution thanDate.now() - Monitor Core Web Vitals (LCP, FID, CLS) - Google ranking factors
- Use PerformanceObserver with
buffered: trueto capture early events - Always check
PerformanceObserver.supportedEntryTypesbefore observing - User Timing API perfect for custom metrics - use descriptive mark/measure names
- Track Resource Timing to identify slow or large assets
- Monitor long tasks (>50ms) to identify responsiveness issues
- Send performance data to analytics - track trends over time, not absolute values
- Clear marks/measures periodically in SPAs to prevent memory growth
- Report metrics on page hide/unload - use
visibilitychangeevent - Test performance on real devices and networks - desktop not representative
- Consider using
web-vitalslibrary for production Web Vitals tracking