Skip to content

Performance API: Precision Dashboard for Web Performance Optimization

Why We Need Performance API

Users don't like waiting. Research shows that for every second increase in page load time, user churn rate rises significantly. But before optimizing performance, you need to be able to measure it accurately—that's why Performance API exists.

Date.now() can tell you the time, but it's only millisecond-precise and susceptible to system clock adjustments. Performance API provides microsecond-level high-precision timing and rich performance metric collection capabilities, letting you clearly see the "skeletal structure" of your web application's performance, like an X-ray.

Performance Object Overview

The performance object is the entry point to Performance API, mounted on the window object:

javascript
// Get performance object
const perf = window.performance;

// View available APIs
console.log(
  "Performance API:",
  Object.getOwnPropertyNames(Performance.prototype)
);

// Basic properties
console.log("Time origin:", performance.timeOrigin);
// Absolute timestamp of page navigation start (Unix time)

console.log("Current time:", performance.now());
// High-precision time relative to timeOrigin

High-Precision Timing: performance.now()

performance.now() returns milliseconds since page navigation started, but with microsecond precision (subject to security restrictions):

javascript
// Basic usage
const startTime = performance.now();

// Execute some operations
for (let i = 0; i < 1000000; i++) {
  Math.sqrt(i);
}

const endTime = performance.now();
console.log(`Execution time: ${endTime - startTime}ms`);
// Output similar to: Execution time: 12.399999998509884ms

// Compare with Date.now()
const dateStart = Date.now();
const perfStart = performance.now();

// Date.now() only has millisecond precision
console.log("Date.now():", dateStart); // 1703841234567
console.log("performance.now():", perfStart); // 1234.567890

Why It's More Precise

javascript
// Advantages of performance.now():
// 1. Not affected by system clock adjustments
// 2. Monotonically increasing, no time reversal
// 3. Microsecond precision (though browsers may reduce precision to prevent specific attacks)

function measureExecution(fn, iterations = 1000) {
  const times = [];

  for (let i = 0; i < iterations; i++) {
    const start = performance.now();
    fn();
    const end = performance.now();
    times.push(end - start);
  }

  // Calculate statistics
  const sum = times.reduce((a, b) => a + b, 0);
  const avg = sum / times.length;
  const sorted = [...times].sort((a, b) => a - b);
  const median = sorted[Math.floor(times.length / 2)];
  const min = sorted[0];
  const max = sorted[sorted.length - 1];

  return { avg, median, min, max };
}

// Usage example
const stringConcatStats = measureExecution(() => {
  let str = "";
  for (let i = 0; i < 100; i++) {
    str += "a";
  }
});

console.log("String concatenation performance:", stringConcatStats);

Performance Marks and Measures

performance.mark() - Create Marks

The mark() method can create marker points on the performance timeline:

javascript
// Create marks
performance.mark("app-start");

// Initialize application...
initializeApp();

performance.mark("app-initialized");

// Load data...
await loadData();

performance.mark("data-loaded");

// Render UI...
renderUI();

performance.mark("ui-rendered");

// View all marks
const marks = performance.getEntriesByType("mark");
console.table(
  marks.map((m) => ({
    name: m.name,
    startTime: m.startTime.toFixed(2) + "ms",
  }))
);

performance.measure() - Measure Intervals

The measure() method can measure time between two marks:

javascript
// Create measure based on two marks
performance.mark("fetch-start");
await fetch("/api/data");
performance.mark("fetch-end");

performance.measure("API Call", "fetch-start", "fetch-end");

// Get measurement results
const measures = performance.getEntriesByType("measure");
measures.forEach((measure) => {
  console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`);
});

// Can also measure from page start to a specific mark
performance.mark("first-paint-complete");
performance.measure("First Paint Time", undefined, "first-paint-complete");

// Or from a mark to current time
performance.mark("load-start");
// ...
performance.measure("Loading in progress", "load-start");

Clearing Marks and Measures

javascript
// Clear specific mark
performance.clearMarks("app-start");

// Clear all marks
performance.clearMarks();

// Clear specific measure
performance.clearMeasures("API Call");

// Clear all measures
performance.clearMeasures();

Practical Application: Performance Tracker

javascript
class PerformanceTracker {
  constructor(prefix = "perf") {
    this.prefix = prefix;
    this.spans = new Map();
  }

  // Start a span
  startSpan(name) {
    const markName = `${this.prefix}:${name}:start`;
    performance.mark(markName);
    this.spans.set(name, markName);
    return markName;
  }

  // End a span
  endSpan(name) {
    const startMark = this.spans.get(name);
    if (!startMark) {
      console.warn(`Span not found: ${name}`);
      return null;
    }

    const endMark = `${this.prefix}:${name}:end`;
    const measureName = `${this.prefix}:${name}`;

    performance.mark(endMark);
    performance.measure(measureName, startMark, endMark);

    this.spans.delete(name);

    const entries = performance.getEntriesByName(measureName, "measure");
    return entries[entries.length - 1]?.duration;
  }

  // Measure function execution time
  async measureAsync(name, fn) {
    this.startSpan(name);
    try {
      return await fn();
    } finally {
      const duration = this.endSpan(name);
      console.log(`[${name}] Duration: ${duration?.toFixed(2)}ms`);
    }
  }

  // Sync version
  measureSync(name, fn) {
    this.startSpan(name);
    try {
      return fn();
    } finally {
      const duration = this.endSpan(name);
      console.log(`[${name}] Duration: ${duration?.toFixed(2)}ms`);
    }
  }

  // Get all measurement results
  getAllMeasures() {
    return performance
      .getEntriesByType("measure")
      .filter((m) => m.name.startsWith(this.prefix))
      .map((m) => ({
        name: m.name.replace(`${this.prefix}:`, ""),
        duration: m.duration,
      }));
  }

  // Get report
  getReport() {
    const measures = this.getAllMeasures();
    const report = {};

    measures.forEach((m) => {
      if (!report[m.name]) {
        report[m.name] = {
          count: 0,
          total: 0,
          min: Infinity,
          max: -Infinity,
        };
      }

      report[m.name].count++;
      report[m.name].total += m.duration;
      report[m.name].min = Math.min(report[m.name].min, m.duration);
      report[m.name].max = Math.max(report[m.name].max, m.duration);
    });

    // Calculate average
    Object.values(report).forEach((r) => {
      r.avg = r.total / r.count;
    });

    return report;
  }

  // Clean up
  clear() {
    performance.clearMarks();
    performance.clearMeasures();
    this.spans.clear();
  }
}

// Usage example
const tracker = new PerformanceTracker("myApp");

// Track API call
await tracker.measureAsync("fetchUsers", async () => {
  const response = await fetch("/api/users");
  return response.json();
});

// Track rendering
tracker.measureSync("renderList", () => {
  // Rendering logic
});

// Get report
console.table(tracker.getReport());

Navigation Timing provides detailed time information for each stage of page navigation:

javascript
// Get navigation time data
const navEntry = performance.getEntriesByType("navigation")[0];

if (navEntry) {
  console.log("Navigation type:", navEntry.type);
  // 'navigate', 'reload', 'back_forward', 'prerender'

  // Key time points
  console.log(
    "DNS lookup time:",
    navEntry.domainLookupEnd - navEntry.domainLookupStart
  );
  console.log("TCP connection time:", navEntry.connectEnd - navEntry.connectStart);
  console.log("Request time:", navEntry.responseStart - navEntry.requestStart);
  console.log("Response time:", navEntry.responseEnd - navEntry.responseStart);
  console.log(
    "DOM parsing time:",
    navEntry.domContentLoadedEventEnd - navEntry.responseEnd
  );
  console.log("Total page load time:", navEntry.loadEventEnd - navEntry.startTime);
}

// Or use legacy API (better compatibility)
const timing = performance.timing;
if (timing) {
  console.log("Page load time:", timing.loadEventEnd - timing.navigationStart);
  console.log(
    "DOM ready time:",
    timing.domContentLoadedEventEnd - timing.navigationStart
  );
}

Page Load Metrics Visualization

javascript
function visualizePageLoadTiming() {
  const nav = performance.getEntriesByType("navigation")[0];

  if (!nav) {
    console.log("Navigation timing data not available");
    return;
  }

  const timeline = [
    {
      phase: "DNS Lookup",
      start: nav.domainLookupStart,
      end: nav.domainLookupEnd,
    },
    { phase: "TCP Connection", start: nav.connectStart, end: nav.connectEnd },
    {
      phase: "SSL Handshake",
      start: nav.secureConnectionStart || nav.connectStart,
      end: nav.connectEnd,
    },
    { phase: "Request Sent", start: nav.requestStart, end: nav.responseStart },
    { phase: "Response Received", start: nav.responseStart, end: nav.responseEnd },
    { phase: "DOM Parsing", start: nav.responseEnd, end: nav.domInteractive },
    {
      phase: "DOMContentLoaded",
      start: nav.domContentLoadedEventStart,
      end: nav.domContentLoadedEventEnd,
    },
    { phase: "Load Event", start: nav.loadEventStart, end: nav.loadEventEnd },
  ];

  console.log("\n📊 Page Load Timeline:");
  console.log("=".repeat(60));

  timeline.forEach(({ phase, start, end }) => {
    const duration = end - start;
    if (duration > 0) {
      const bar = "█".repeat(Math.min(Math.round(duration / 10), 40));
      console.log(`${phase.padEnd(18)} ${bar} ${duration.toFixed(2)}ms`);
    }
  });

  console.log("=".repeat(60));
  console.log(`Total load time: ${nav.loadEventEnd.toFixed(2)}ms`);
}

// Execute after page load
window.addEventListener("load", () => {
  setTimeout(visualizePageLoadTiming, 0);
});

Resource Timing API

Resource Timing provides detailed load time information for each resource:

javascript
// Get all resource load times
const resources = performance.getEntriesByType("resource");

console.log(`Loaded ${resources.length} resources`);

resources.forEach((resource) => {
  console.log({
    name: resource.name,
    type: resource.initiatorType,
    duration: resource.duration.toFixed(2) + "ms",
    size: resource.transferSize + " bytes",
  });
});

// Group statistics by type
function analyzeResourcesByType() {
  const resources = performance.getEntriesByType("resource");
  const byType = {};

  resources.forEach((r) => {
    if (!byType[r.initiatorType]) {
      byType[r.initiatorType] = {
        count: 0,
        totalDuration: 0,
        totalSize: 0,
      };
    }

    byType[r.initiatorType].count++;
    byType[r.initiatorType].totalDuration += r.duration;
    byType[r.initiatorType].totalSize += r.transferSize || 0;
  });

  return byType;
}

console.table(analyzeResourcesByType());

Find Slow Resources

javascript
function findSlowResources(threshold = 500) {
  const resources = performance.getEntriesByType("resource");

  const slowResources = resources
    .filter((r) => r.duration > threshold)
    .sort((a, b) => b.duration - a.duration)
    .map((r) => ({
      url: r.name.split("/").pop(), // Show filename only
      fullUrl: r.name,
      type: r.initiatorType,
      duration: r.duration.toFixed(2) + "ms",
      size: (r.transferSize / 1024).toFixed(2) + "KB",
    }));

  if (slowResources.length > 0) {
    console.warn(
      `⚠️ Found ${slowResources.length} slow resources (>${threshold}ms):`
    );
    console.table(slowResources);
  } else {
    console.log(`✓ All resources loaded within ${threshold}ms`);
  }

  return slowResources;
}

// Check after page load
window.addEventListener("load", () => {
  setTimeout(() => findSlowResources(300), 100);
});

First Paint and Largest Contentful Paint

Modern browsers provide Paint Timing API to measure key rendering metrics:

javascript
// Get paint timings
function getPaintTimings() {
  const paintEntries = performance.getEntriesByType("paint");

  const timings = {};
  paintEntries.forEach((entry) => {
    timings[entry.name] = entry.startTime;
  });

  console.log("First Paint:", timings["first-paint"]?.toFixed(2) + "ms");
  console.log(
    "First Contentful Paint:",
    timings["first-contentful-paint"]?.toFixed(2) + "ms"
  );

  return timings;
}

// Use PerformanceObserver to listen for LCP
function observeLCP() {
  const observer = new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];

    console.log(
      "Largest Contentful Paint:",
      lastEntry.startTime.toFixed(2) + "ms"
    );
    console.log("LCP element:", lastEntry.element);
  });

  observer.observe({ type: "largest-contentful-paint", buffered: true });
}

// Check after page load
window.addEventListener("load", () => {
  getPaintTimings();
  observeLCP();
});

PerformanceObserver

PerformanceObserver can asynchronously observe creation of performance entries:

javascript
// Observe all performance entries
const observer = new PerformanceObserver((list, observer) => {
  const entries = list.getEntries();

  entries.forEach((entry) => {
    console.log(
      `[${entry.entryType}] ${entry.name}: ${
        entry.duration?.toFixed(2) || entry.startTime.toFixed(2)
      }ms`
    );
  });
});

// Observe multiple types
observer.observe({
  entryTypes: ["mark", "measure", "resource", "navigation"],
});

// Observe specific type only (more efficient)
const resourceObserver = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    if (entry.duration > 1000) {
      console.warn(`Slow resource: ${entry.name} (${entry.duration.toFixed(0)}ms)`);
    }
  });
});

resourceObserver.observe({ type: "resource", buffered: true });

// Stop observing
// observer.disconnect();

Monitor Long Tasks

javascript
// Monitor long tasks (tasks exceeding 50ms)
function observeLongTasks() {
  if (!PerformanceObserver.supportedEntryTypes.includes("longtask")) {
    console.log("Browser doesn't support Long Task monitoring");
    return;
  }

  const observer = new PerformanceObserver((list) => {
    list.getEntries().forEach((entry) => {
      console.warn("⚠️ Long task detected:", {
        duration: entry.duration.toFixed(2) + "ms",
        startTime: entry.startTime.toFixed(2) + "ms",
        name: entry.name,
      });
    });
  });

  observer.observe({ type: "longtask", buffered: true });
  return observer;
}

observeLongTasks();

Performance Monitoring System

Combine the above APIs to build a complete performance monitoring system:

javascript
class PerformanceMonitor {
  constructor(options = {}) {
    this.metrics = {};
    this.options = {
      reportUrl: options.reportUrl,
      sampleRate: options.sampleRate || 1,
      ...options,
    };

    this.init();
  }

  init() {
    // Sampling check
    if (Math.random() > this.options.sampleRate) {
      return;
    }

    this.collectNavigationTiming();
    this.collectPaintTiming();
    this.observeResources();
    this.observeLongTasks();
    this.collectWebVitals();

    // Send data on page unload
    window.addEventListener("beforeunload", () => {
      this.sendReport();
    });

    // Or send periodically
    // setInterval(() => this.sendReport(), 60000);
  }

  collectNavigationTiming() {
    window.addEventListener("load", () => {
      setTimeout(() => {
        const nav = performance.getEntriesByType("navigation")[0];
        if (nav) {
          this.metrics.navigation = {
            dns: nav.domainLookupEnd - nav.domainLookupStart,
            tcp: nav.connectEnd - nav.connectStart,
            ttfb: nav.responseStart - nav.requestStart,
            download: nav.responseEnd - nav.responseStart,
            domParse: nav.domInteractive - nav.responseEnd,
            domReady: nav.domContentLoadedEventEnd - nav.startTime,
            load: nav.loadEventEnd - nav.startTime,
          };
        }
      }, 0);
    });
  }

  collectPaintTiming() {
    window.addEventListener("load", () => {
      setTimeout(() => {
        const paints = performance.getEntriesByType("paint");
        paints.forEach((paint) => {
          this.metrics[paint.name] = paint.startTime;
        });
      }, 0);
    });
  }

  observeResources() {
    const observer = new PerformanceObserver((list) => {
      if (!this.metrics.resources) {
        this.metrics.resources = {
          count: 0,
          totalSize: 0,
          totalDuration: 0,
          slow: [],
        };
      }

      list.getEntries().forEach((entry) => {
        this.metrics.resources.count++;
        this.metrics.resources.totalSize += entry.transferSize || 0;
        this.metrics.resources.totalDuration += entry.duration;

        if (entry.duration > 1000) {
          this.metrics.resources.slow.push({
            url: entry.name,
            duration: entry.duration,
            size: entry.transferSize,
          });
        }
      });
    });

    observer.observe({ type: "resource", buffered: true });
  }

  observeLongTasks() {
    if (!PerformanceObserver.supportedEntryTypes.includes("longtask")) {
      return;
    }

    this.metrics.longTasks = [];

    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        this.metrics.longTasks.push({
          startTime: entry.startTime,
          duration: entry.duration,
        });
      });
    });

    observer.observe({ type: "longtask", buffered: true });
  }

  collectWebVitals() {
    // LCP
    if (
      PerformanceObserver.supportedEntryTypes.includes(
        "largest-contentful-paint"
      )
    ) {
      const lcpObserver = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        this.metrics.lcp = entries[entries.length - 1]?.startTime;
      });
      lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
    }

    // FID (requires user interaction to trigger)
    if (PerformanceObserver.supportedEntryTypes.includes("first-input")) {
      const fidObserver = new PerformanceObserver((list) => {
        const entry = list.getEntries()[0];
        this.metrics.fid = entry.processingStart - entry.startTime;
      });
      fidObserver.observe({ type: "first-input", buffered: true });
    }

    // CLS
    if (PerformanceObserver.supportedEntryTypes.includes("layout-shift")) {
      let clsValue = 0;
      const clsObserver = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          if (!entry.hadRecentInput) {
            clsValue += entry.value;
          }
        });
        this.metrics.cls = clsValue;
      });
      clsObserver.observe({ type: "layout-shift", buffered: true });
    }
  }

  getReport() {
    return {
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
      metrics: this.metrics,
    };
  }

  sendReport() {
    const report = this.getReport();

    if (this.options.reportUrl) {
      // Use sendBeacon to ensure data is sent
      const blob = new Blob([JSON.stringify(report)], {
        type: "application/json",
      });
      navigator.sendBeacon(this.options.reportUrl, blob);
    }

    console.log("📊 Performance report:", report);
    return report;
  }
}

// Usage example
const monitor = new PerformanceMonitor({
  reportUrl: "/api/performance",
  sampleRate: 0.1, // 10% sampling rate
});

// Manually get report
setTimeout(() => {
  console.table(monitor.getReport().metrics);
}, 5000);

Performance Optimization Practices

Identify Performance Bottlenecks

javascript
function diagnosePerformance() {
  const nav = performance.getEntriesByType("navigation")[0];
  const issues = [];

  // Check DNS
  if (nav.domainLookupEnd - nav.domainLookupStart > 100) {
    issues.push({
      type: "DNS",
      message: "DNS lookup time too long, consider using DNS prefetch",
      solution: '<link rel="dns-prefetch" href="//your-domain.com">',
    });
  }

  // Check TTFB
  if (nav.responseStart - nav.requestStart > 500) {
    issues.push({
      type: "TTFB",
      message: "TTFB too long, slow server response",
      solution: "Optimize server-side processing, use CDN, enable caching",
    });
  }

  // Check resource size
  const resources = performance.getEntriesByType("resource");
  const largeResources = resources.filter(
    (r) => (r.transferSize || 0) > 500 * 1024
  );
  if (largeResources.length > 0) {
    issues.push({
      type: "RESOURCE_SIZE",
      message: `${largeResources.length} resources over 500KB`,
      solution: "Compress resources, optimize images, lazy load",
      details: largeResources.map((r) => r.name),
    });
  }

  // Check resource count
  if (resources.length > 100) {
    issues.push({
      type: "RESOURCE_COUNT",
      message: `Too many resource requests (${resources.length})`,
      solution: "Merge resources, use HTTP/2, lazy loading",
    });
  }

  return issues;
}

// Run diagnostics
window.addEventListener("load", () => {
  setTimeout(() => {
    const issues = diagnosePerformance();
    if (issues.length > 0) {
      console.warn("🔍 Performance issues found:");
      issues.forEach((issue) => {
        console.group(`❌ ${issue.type}`);
        console.log("Problem:", issue.message);
        console.log("Suggestion:", issue.solution);
        if (issue.details) {
          console.log("Details:", issue.details);
        }
        console.groupEnd();
      });
    } else {
      console.log("✅ Performance check passed!");
    }
  }, 1000);
});

Summary

Performance API is an important tool for building high-performance web applications. It provides comprehensive capabilities from coarse-grained page load times to fine-grained resource load analysis, from simple code execution timing to complex Web Vitals metric collection.

Key takeaways:

  • performance.now() provides high-precision timing
  • mark() and measure() for custom performance markers
  • Navigation Timing analyzes page load stages
  • Resource Timing tracks each resource's load performance
  • PerformanceObserver implements asynchronous performance monitoring
  • Build complete performance monitoring systems with Web Vitals

Master these tools to accurately identify performance bottlenecks, make data-driven optimization decisions, and ultimately provide users with a smooth and fast experience.