← Back to Index

Lesson 11

Performance & Tracing

What You'll Learn

Two domains cover performance analysis in CDP:

These are different tools for different jobs. Performance gives you quick numbers; Tracing gives you deep analysis.

The Performance Domain - Runtime Metrics

Enabling and reading metrics

// Enable metric collection
await send('Performance.enable');

// Get current metric values at any time
const metrics = await send('Performance.getMetrics');
// metrics.metrics = [
//   { name: 'Timestamp', value: 1719012345.678 },
//   { name: 'Documents', value: 1 },
//   { name: 'Frames', value: 1 },
//   { name: 'JSEventListeners', value: 47 },
//   { name: 'Nodes', value: 312 },
//   { name: 'LayoutCount', value: 14 },
//   { name: 'RecalcStyleCount', value: 18 },
//   { name: 'LayoutDuration', value: 0.023 },
//   { name: 'RecalcStyleDuration', value: 0.011 },
//   { name: 'ScriptDuration', value: 0.156 },
//   { name: 'TaskDuration', value: 0.312 },
//   { name: 'JSHeapUsedSize', value: 8453120 },
//   { name: 'JSHeapTotalSize', value: 12582912 },
//   ... more metrics
// ]

Key metrics explained

Metric What it tells you
Nodes Total DOM nodes. High/growing = potential memory leak or excessive DOM
JSHeapUsedSize JS memory in use (bytes). Growing over time = memory leak
JSHeapTotalSize Total JS heap allocated. Much larger than Used = fragmentation
JSEventListeners Number of active event listeners. Growing = listeners not being cleaned up
LayoutCount Number of layout recalculations. High = layout thrashing
RecalcStyleCount Number of style recalculations. High = excessive CSS changes
ScriptDuration Time spent executing JS (seconds). High = expensive computations
TaskDuration Total time for all tasks (JS + layout + style + paint). The "main thread busy" metric

Taking metric snapshots during a journey

You can call getMetrics at different points during a journey to detect performance regressions:

// Capture before an action
const before = await send('Performance.getMetrics');

// Perform the action (navigate, click, load data)
await send('Page.navigate', { url: 'http://localhost:4200/dashboard' });
await waitForSelector('[data-testid="dashboard-loaded"]');

// Capture after
const after = await send('Performance.getMetrics');

// Compare
function getMetricValue(metrics, name) {
  const metric = metrics.metrics.find(m => m.name === name);
  return metric ? metric.value : 0;
}

const heapGrowth = getMetricValue(after, 'JSHeapUsedSize') - getMetricValue(before, 'JSHeapUsedSize');
const nodeGrowth = getMetricValue(after, 'Nodes') - getMetricValue(before, 'Nodes');

console.log(`Heap grew by: ${(heapGrowth / 1024 / 1024).toFixed(2)} MB`);
console.log(`DOM nodes added: ${nodeGrowth}`);
For the package: You could capture metrics before and after each journey step and flag anomalies (huge heap growth, thousands of new DOM nodes, excessive layout recalculations). This is lightweight - one CDP call per measurement - and gives early warning of performance issues without full tracing.

The Tracing Domain - Full Trace Recording

The Tracing domain records the same data you see in DevTools' Performance panel - a complete timeline of everything the browser did: JavaScript execution, layout, paint, network, compositor activity, GPU work.

Recording a trace

// Collect trace events as they arrive
const traceEvents = [];

// Listen for trace data chunks
// (Tracing.dataCollected fires multiple times with batches of events)
ws.on('message', (data) => {
  const msg = JSON.parse(data.toString());
  if (msg.method === 'Tracing.dataCollected') {
    traceEvents.push(...msg.params.value);
  }
});

// Start recording
await send('Tracing.start', {
  categories: '-*,devtools.timeline,v8.execute,disabled-by-default-devtools.timeline',
  transferMode: 'ReportEvents'   // Send events as they happen
});

// ... perform the actions you want to profile ...
await send('Page.navigate', { url: 'http://localhost:4200' });
await waitForSelector('[data-testid="app-ready"]');

// Stop recording
await send('Tracing.end');

// Wait for tracingComplete event (signals all data has been sent)
await new Promise((resolve) => {
  const handler = (data) => {
    const msg = JSON.parse(data.toString());
    if (msg.method === 'Tracing.tracingComplete') {
      ws.removeListener('message', handler);
      resolve();
    }
  };
  ws.on('message', handler);
});

Trace categories

Categories control what gets recorded. Common useful categories:

The -* prefix disables all categories first, then you selectively enable what you need. Without it, you get everything - which can be enormous.

To see all available categories:

const categories = await send('Tracing.getCategories');
// categories.categories = ['devtools.timeline', 'v8', 'blink', 'cc', 'gpu', ...]

Saving the trace to a file

The trace data is in Chrome's trace format - you can save it as JSON and load it directly into DevTools' Performance panel (or Perfetto) for visual analysis:

import { writeFileSync } from 'fs';

// Chrome trace format is just a JSON array of trace events
const traceFile = JSON.stringify(traceEvents);
writeFileSync('trace.json', traceFile);

console.log(`Trace saved: ${traceEvents.length} events`);
// Open in DevTools: Performance panel → Load profile → select trace.json
// Or open https://ui.perfetto.dev and drag the file in

Analysing trace data programmatically

Each trace event has this shape:

{
  "cat": "devtools.timeline",        // Category
  "name": "FunctionCall",             // Event type
  "ph": "X",                          // Phase: 'X' = complete, 'B' = begin, 'E' = end
  "ts": 1234567890,                   // Timestamp (microseconds)
  "dur": 5000,                        // Duration (microseconds) - for complete events
  "pid": 1234,                        // Process ID
  "tid": 5678,                        // Thread ID
  "args": {                           // Event-specific data
    "data": {
      "functionName": "renderComponent",
      "scriptId": "7",
      "url": "http://localhost:4200/main.js",
      "lineNumber": 42
    }
  }
}

You can analyse this programmatically to find performance bottlenecks:

// Find the longest JavaScript functions
const longFunctions = traceEvents
  .filter(event => event.name === 'FunctionCall' && event.dur)
  .sort((a, b) => b.dur - a.dur)
  .slice(0, 10)
  .map(event => ({
    name: event.args?.data?.functionName || 'anonymous',
    duration: `${(event.dur / 1000).toFixed(1)}ms`,
    url: event.args?.data?.url
  }));

console.log('Top 10 slowest functions:', longFunctions);

// Find layout thrashing (forced reflows)
const forcedLayouts = traceEvents
  .filter(event => event.name === 'Layout' && event.args?.beginData?.dirtyObjects > 0);

console.log(`Forced layouts: ${forcedLayouts.length}`);

// Find long tasks (> 50ms - these block the main thread)
const longTasks = traceEvents
  .filter(event => event.name === 'RunTask' && event.dur > 50000);  // 50ms in microseconds

console.log(`Long tasks (> 50ms): ${longTasks.length}`);
User timing marks: If an Angular app uses performance.mark('route-loaded') and performance.measure(), these show up in the trace under the blink.user_timing category. You could instrument the application with strategic marks and then programmatically read them from the trace to measure specific user-perceived performance metrics.

Performance vs Tracing: When to Use Which

Performance domain Tracing domain
Data type Point-in-time metrics (current values) Full timeline of events
Overhead Near zero Moderate (recording impacts performance)
Output size A few KB (metric values) Potentially hundreds of MB for long recordings
Good for Quick health checks, regression detection, CI assertions Deep investigation, understanding why something is slow
Always-on? Yes - lightweight enough to leave enabled No - enable only when investigating
Analogy Car dashboard (speed, fuel, temperature) Black box flight recorder

Practical Use

For a CDP-based journey runner:

// In the package's diagnostic collector:
class DiagnosticCollector {
  // ...existing code...

  async captureMetricsSnapshot(label) {
    const metrics = await this.send('Performance.getMetrics');
    this.#metricsSnapshots = [
      ...this.#metricsSnapshots,
      { label, timestamp: Date.now(), metrics: metrics.metrics }
    ];
  }

  getMetricsReport() {
    return this.#metricsSnapshots;
  }
}

Key Commands Reference

Command Purpose
Performance.enable Start collecting metrics
Performance.getMetrics Read current metric values
Tracing.start Begin recording a trace (specify categories)
Tracing.end Stop recording
Tracing.getCategories List all available trace categories

For the full list, see the Performance and Tracing sections of the complete reference.

Primary Source

Read the Performance domain documentation and the Tracing domain documentation. For understanding trace file format, see Chrome's Trace Event Format documentation.