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 than Date.now()
  • Monitor Core Web Vitals (LCP, FID, CLS) - Google ranking factors
  • Use PerformanceObserver with buffered: true to capture early events
  • Always check PerformanceObserver.supportedEntryTypes before 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 visibilitychange event
  • Test performance on real devices and networks - desktop not representative
  • Consider using web-vitals library for production Web Vitals tracking