JavaScript Profiling: Advanced Performance Guide

An advanced guide to JavaScript profiling and performance analysis. Covers the Performance API, PerformanceObserver, User Timing API, Long Tasks, Layout Shifts, resource timing, flame chart analysis, CPU profiling patterns, and building a custom performance monitor.

JavaScriptadvanced
18 min read

Profiling identifies where your JavaScript code spends time, which resources block rendering, and what operations cause layout shifts or long tasks. This guide covers the browser Performance APIs, custom timing instrumentation, and building automated monitoring.

For DevTools-specific workflows, see Using Chrome DevTools for JS Performance Tuning.

Performance API: High-Resolution Timing

javascriptjavascript
// performance.now() provides sub-millisecond timestamps
function measureExecution(fn, label = "operation") {
  const start = performance.now();
  const result = fn();
  const end = performance.now();
 
  console.log(`${label}: ${(end - start).toFixed(3)}ms`);
  return result;
}
 
// Async version
async function measureAsync(fn, label = "async-operation") {
  const start = performance.now();
  const result = await fn();
  const end = performance.now();
 
  console.log(`${label}: ${(end - start).toFixed(3)}ms`);
  return result;
}
 
// Usage
measureExecution(() => {
  const arr = Array.from({ length: 100000 }, (_, i) => i);
  return arr.sort(() => Math.random() - 0.5);
}, "sort-100k");
 
await measureAsync(async () => {
  return fetch("/api/data").then((r) => r.json());
}, "api-fetch");

User Timing API (performance.mark and measure)

javascriptjavascript
class PerformanceTracker {
  constructor(prefix = "app") {
    this.prefix = prefix;
  }
 
  mark(name) {
    performance.mark(`${this.prefix}:${name}`);
  }
 
  measure(name, startMark, endMark) {
    const measureName = `${this.prefix}:${name}`;
    const start = `${this.prefix}:${startMark}`;
    const end = `${this.prefix}:${endMark}`;
 
    try {
      performance.measure(measureName, start, end);
      const entries = performance.getEntriesByName(measureName);
      return entries[entries.length - 1];
    } catch (error) {
      console.warn("Measure failed:", error.message);
      return null;
    }
  }
 
  measureFromNavigation(name, markName) {
    // Measure from page navigation start
    const mark = `${this.prefix}:${markName}`;
    const measureName = `${this.prefix}:${name}`;
 
    performance.measure(measureName, { start: 0, end: mark });
    return performance.getEntriesByName(measureName).pop();
  }
 
  getAllMeasures() {
    return performance.getEntriesByType("measure")
      .filter((e) => e.name.startsWith(this.prefix))
      .map((e) => ({
        name: e.name.replace(`${this.prefix}:`, ""),
        duration: parseFloat(e.duration.toFixed(3)),
        startTime: parseFloat(e.startTime.toFixed(3)),
      }));
  }
 
  clear() {
    performance.clearMarks();
    performance.clearMeasures();
  }
}
 
// Usage
const perf = new PerformanceTracker("myapp");
 
perf.mark("render-start");
renderComponent();
perf.mark("render-end");
 
const measure = perf.measure("render", "render-start", "render-end");
console.log(`Render took ${measure.duration.toFixed(2)}ms`);

PerformanceObserver for Real-Time Monitoring

javascriptjavascript
class PerformanceMonitor {
  constructor() {
    this.observers = [];
    this.data = {
      longTasks: [],
      layoutShifts: [],
      paints: [],
      resources: [],
    };
  }
 
  observeLongTasks() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        this.data.longTasks.push({
          duration: entry.duration,
          startTime: entry.startTime,
          name: entry.name,
        });
 
        if (entry.duration > 100) {
          console.warn(`Long task: ${entry.duration.toFixed(0)}ms`);
        }
      }
    });
 
    observer.observe({ type: "longtask", buffered: true });
    this.observers.push(observer);
  }
 
  observeLayoutShifts() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          this.data.layoutShifts.push({
            value: entry.value,
            startTime: entry.startTime,
            sources: entry.sources?.map((s) => ({
              node: s.node?.tagName,
              previousRect: s.previousRect,
              currentRect: s.currentRect,
            })),
          });
        }
      }
    });
 
    observer.observe({ type: "layout-shift", buffered: true });
    this.observers.push(observer);
  }
 
  observePaints() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        this.data.paints.push({
          name: entry.name,
          startTime: entry.startTime,
        });
      }
    });
 
    observer.observe({ type: "paint", buffered: true });
    this.observers.push(observer);
  }
 
  observeResources(pattern = null) {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (pattern && !entry.name.match(pattern)) continue;
 
        this.data.resources.push({
          name: entry.name,
          type: entry.initiatorType,
          duration: entry.duration,
          transferSize: entry.transferSize,
          decodedSize: entry.decodedBodySize,
        });
      }
    });
 
    observer.observe({ type: "resource", buffered: true });
    this.observers.push(observer);
  }
 
  getCLS() {
    return this.data.layoutShifts
      .reduce((sum, shift) => sum + shift.value, 0);
  }
 
  getLongTaskCount() {
    return this.data.longTasks.length;
  }
 
  getTotalBlockingTime() {
    return this.data.longTasks.reduce((total, task) => {
      return total + Math.max(0, task.duration - 50);
    }, 0);
  }
 
  getReport() {
    return {
      cls: this.getCLS().toFixed(4),
      longTasks: this.getLongTaskCount(),
      totalBlockingTime: `${this.getTotalBlockingTime().toFixed(0)}ms`,
      paints: this.data.paints,
      slowResources: this.data.resources
        .filter((r) => r.duration > 500)
        .sort((a, b) => b.duration - a.duration)
        .slice(0, 10),
    };
  }
 
  disconnect() {
    this.observers.forEach((o) => o.disconnect());
    this.observers = [];
  }
}
 
// Usage
const monitor = new PerformanceMonitor();
monitor.observeLongTasks();
monitor.observeLayoutShifts();
monitor.observePaints();
monitor.observeResources();
 
// Report after page is loaded
window.addEventListener("load", () => {
  setTimeout(() => console.log(monitor.getReport()), 3000);
});

Function-Level Profiling

javascriptjavascript
function profileFunction(fn, name) {
  let callCount = 0;
  let totalTime = 0;
  let maxTime = 0;
  let minTime = Infinity;
 
  const profiled = function (...args) {
    const start = performance.now();
    const result = fn.apply(this, args);
    const elapsed = performance.now() - start;
 
    callCount++;
    totalTime += elapsed;
    maxTime = Math.max(maxTime, elapsed);
    minTime = Math.min(minTime, elapsed);
 
    return result;
  };
 
  profiled.getStats = () => ({
    name,
    calls: callCount,
    totalMs: totalTime.toFixed(3),
    avgMs: callCount > 0 ? (totalTime / callCount).toFixed(3) : "0",
    maxMs: maxTime.toFixed(3),
    minMs: minTime === Infinity ? "0" : minTime.toFixed(3),
  });
 
  profiled.reset = () => {
    callCount = 0;
    totalTime = 0;
    maxTime = 0;
    minTime = Infinity;
  };
 
  return profiled;
}
 
// Profile async functions
function profileAsync(fn, name) {
  let callCount = 0;
  let totalTime = 0;
  let maxTime = 0;
 
  const profiled = async function (...args) {
    const start = performance.now();
    const result = await fn.apply(this, args);
    const elapsed = performance.now() - start;
 
    callCount++;
    totalTime += elapsed;
    maxTime = Math.max(maxTime, elapsed);
 
    return result;
  };
 
  profiled.getStats = () => ({
    name,
    calls: callCount,
    totalMs: totalTime.toFixed(3),
    avgMs: callCount > 0 ? (totalTime / callCount).toFixed(3) : "0",
    maxMs: maxTime.toFixed(3),
  });
 
  return profiled;
}
 
// Usage
const profiledSort = profileFunction(
  (arr) => arr.slice().sort((a, b) => a - b),
  "numericSort"
);
 
for (let i = 0; i < 100; i++) {
  profiledSort(Array.from({ length: 10000 }, () => Math.random()));
}
 
console.table([profiledSort.getStats()]);

Resource Timing Analysis

javascriptjavascript
function analyzeResourceTiming() {
  const resources = performance.getEntriesByType("resource");
 
  const byType = {};
 
  resources.forEach((r) => {
    const type = r.initiatorType;
    if (!byType[type]) {
      byType[type] = { count: 0, totalDuration: 0, totalSize: 0 };
    }
 
    byType[type].count++;
    byType[type].totalDuration += r.duration;
    byType[type].totalSize += r.transferSize || 0;
  });
 
  // Format results
  const report = Object.entries(byType).map(([type, stats]) => ({
    type,
    count: stats.count,
    avgDurationMs: (stats.totalDuration / stats.count).toFixed(1),
    totalSizeKB: (stats.totalSize / 1024).toFixed(1),
  }));
 
  return report.sort((a, b) => parseFloat(b.avgDurationMs) - parseFloat(a.avgDurationMs));
}
 
// Find slow resources
function findSlowResources(thresholdMs = 500) {
  return performance.getEntriesByType("resource")
    .filter((r) => r.duration > thresholdMs)
    .map((r) => ({
      url: new URL(r.name).pathname,
      type: r.initiatorType,
      duration: `${r.duration.toFixed(0)}ms`,
      size: `${((r.transferSize || 0) / 1024).toFixed(1)}KB`,
      ttfb: `${(r.responseStart - r.requestStart).toFixed(0)}ms`,
    }))
    .sort((a, b) => parseFloat(b.duration) - parseFloat(a.duration));
}
 
console.table(analyzeResourceTiming());
console.table(findSlowResources());
Timing PropertyWhat It Measures
startTimeWhen the resource request was initiated
domainLookupEnd - domainLookupStartDNS lookup time
connectEnd - connectStartTCP connection time
responseStart - requestStartTime to First Byte (TTFB)
responseEnd - responseStartDownload time
durationTotal time from start to response end
transferSizeBytes transferred over the network
decodedBodySizeDecoded (uncompressed) body size

Custom Performance Dashboard

javascriptjavascript
class PerfDashboard {
  constructor(containerId) {
    this.container = document.getElementById(containerId);
    this.monitor = new PerformanceMonitor();
    this.intervalId = null;
  }
 
  start() {
    this.monitor.observeLongTasks();
    this.monitor.observeLayoutShifts();
    this.monitor.observePaints();
 
    this.intervalId = setInterval(() => this.render(), 2000);
    this.render();
  }
 
  render() {
    const report = this.monitor.getReport();
    const mem = performance.memory ? {
      used: (performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1),
      total: (performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1),
    } : { used: "N/A", total: "N/A" };
 
    this.container.innerHTML = `
      <div class="perf-dashboard">
        <div class="metric">
          <span class="label">CLS</span>
          <span class="value">${report.cls}</span>
        </div>
        <div class="metric">
          <span class="label">Long Tasks</span>
          <span class="value">${report.longTasks}</span>
        </div>
        <div class="metric">
          <span class="label">TBT</span>
          <span class="value">${report.totalBlockingTime}</span>
        </div>
        <div class="metric">
          <span class="label">Heap Used</span>
          <span class="value">${mem.used} MB</span>
        </div>
        <div class="metric">
          <span class="label">Heap Total</span>
          <span class="value">${mem.total} MB</span>
        </div>
      </div>
    `;
  }
 
  stop() {
    if (this.intervalId) clearInterval(this.intervalId);
    this.monitor.disconnect();
  }
}
 
// Usage
const dashboard = new PerfDashboard("perf-panel");
dashboard.start();
Rune AI

Rune AI

Key Insights

  • performance.now() for high-resolution timing: Use it instead of Date.now() for profiling; it provides microsecond precision and monotonic clock guarantees
  • User Timing API for structured measurement: Create mark() and measure() entries that appear in DevTools Performance recordings and can be collected by PerformanceObserver
  • PerformanceObserver for automated monitoring: Observe Long Tasks, Layout Shifts, and Resource Timing in real-time without polling, with buffered support for entries before observer registration
  • Total Blocking Time quantifies responsiveness: Sum the time beyond 50ms for all Long Tasks to get TBT, a key metric that correlates with perceived input latency
  • Function-level profiling wraps target functions: Create wrapper functions that track call count, total time, average, min, and max to identify hot functions without DevTools
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between performance.now() and Date.now()?

`performance.now()` returns a high-resolution timestamp (microsecond precision) relative to the page navigation start. `Date.now()` returns millisecond-resolution Unix timestamp. For profiling, always use `performance.now()` because it is monotonic (not affected by system clock changes) and offers higher precision. See [How to Measure JavaScript Execution Time Accurately](/tutorials/programming-languages/javascript/how-to-measure-javascript-execution-time-accurately) for detailed comparisons.

What counts as a Long Task?

Long Task is any task that takes more than 50ms on the main thread. This includes JavaScript execution, layout calculations, paint operations, and garbage collection. Long Tasks delay user input processing, causing the interface to feel unresponsive. Total Blocking Time (TBT) is the sum of the time beyond 50ms for all long tasks.

Can I profile Web Workers with the Performance API?

Yes. Web Workers have their own `performance` object with `performance.now()`, `performance.mark()`, and `performance.measure()`. You can post timing data back to the main thread via `postMessage`. PerformanceObserver also works inside workers for observing resource timing and user timing entries.

How do I correlate profiling data with user interactions?

Use `performance.mark()` on user events (click handlers, form submissions, navigation) to create named timestamps. Then use `performance.measure()` to compute durations between user actions and rendering completions. The Event Timing API (`PerformanceObserver` with type `"event"`) provides built-in interaction-to-paint measurements.

Does the Performance API add overhead to my application?

The overhead is minimal. `performance.now()` calls take nanoseconds. `PerformanceObserver` runs asynchronously and does not block the main thread. `performance.mark()` and `performance.measure()` add entries to an internal buffer. The main risk is memory from accumulating entries; use `performance.clearMarks()` and `performance.clearMeasures()` periodically.

Conclusion

JavaScript profiling combines the Performance API (performance.now(), mark(), measure()), PerformanceObserver (Long Tasks, Layout Shifts, Resource Timing), and custom instrumentation (function-level profiling, async measurement) to identify bottlenecks. Monitor CLS, TBT, and heap usage in real-time, automate measurements with PerformanceObserver, and correlate timing with user interactions. For DevTools-based profiling, see Using Chrome DevTools for JS Performance Tuning. For accurate timing techniques, see How to Measure JavaScript Execution Time Accurately.