Lesson 11
Performance & Tracing
What You'll Learn
Two domains cover performance analysis in CDP:
Performance- lightweight runtime metrics (JS heap size, DOM node count, layout count, etc.). Think "dashboard gauges."Tracing- full Chrome trace recording (the same data you see in DevTools' Performance panel). Think "flight recorder."
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}`);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:
devtools.timeline- main thread activity (JS, layout, paint, compositing)v8.execute- V8 JavaScript execution detailsdisabled-by-default-devtools.timeline- additional timeline data (stack traces for events)disabled-by-default-devtools.timeline.frame- frame rendering datadisabled-by-default-v8.cpu_profiler- CPU profile samplesblink.user_timing-performance.mark()andperformance.measure()from your code
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 inAnalysing 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}`);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:
- Performance metrics: Always enabled. Capture before/after each journey step. Flag anomalies (heap growing, node count exploding). Include in diagnostic reports.
- Tracing: Opt-in. When a journey is slow and you need to understand why, enable tracing for that run and save the trace file for manual analysis in DevTools or Perfetto.
// 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.