Skip to content

Custom Metrics

Track custom timing metrics beyond React renders.

Overview

While React Profiler captures render metrics, you often need to track other operations:

  • API fetch times
  • Animation durations
  • Multi-step wizard flows
  • Time to interactive

Custom metrics use the Performance API's mark() and measure() pattern.

Basic Usage

Creating Marks

Marks are timestamps that record when something happened:

test.performance({
  thresholds: { base: { profiler: { '*': { duration: 500, rerenders: 20 } } } },
})('custom timing', async ({ page, performance }) => {
  await page.goto('/');
  await performance.init();
 
  performance.mark('operation-start');
  await page.click('button[data-testid="load"]');
  await page.waitForSelector('.loaded');
  performance.mark('operation-end');
});

Creating Measures

Measures calculate duration between two marks:

performance.mark('fetch-start');
await page.click('button[data-testid="load"]');
await page.waitForSelector('.loaded');
performance.mark('fetch-end');
 
const duration = performance.measure('data-fetch', 'fetch-start', 'fetch-end');
console.log(`Fetch took ${duration}ms`);

The measure() method returns the duration in milliseconds.

Tracking Multiple Operations

test.performance({
  thresholds: { base: { profiler: { '*': { duration: 500, rerenders: 20 } } } },
})('data loading flow', async ({ page, performance }) => {
  await page.goto('/dashboard');
  await performance.init();
 
  // Track fetch operation
  performance.mark('fetch-start');
  await page.click('[data-testid="load-data"]');
  await page.waitForSelector('.data-loaded');
  performance.mark('fetch-end');
 
  // Track render operation
  performance.mark('render-start');
  await performance.waitUntilStable();
  performance.mark('render-end');
 
  // Create measures
  const fetchTime = performance.measure('data-fetch', 'fetch-start', 'fetch-end');
  const renderTime = performance.measure('render-time', 'render-start', 'render-end');
 
  console.log(`Fetch: ${fetchTime}ms, Render: ${renderTime}ms`);
});

Multi-Step Flows

Track each step in a multi-step process:

test.performance({
  thresholds: { base: { profiler: { '*': { duration: 800, rerenders: 25 } } } },
})('wizard flow', async ({ page, performance }) => {
  await page.goto('/wizard');
  await performance.init();
 
  // Step 1
  performance.mark('step1-start');
  await page.click('[data-testid="next"]');
  await performance.waitUntilStable();
  performance.mark('step1-end');
 
  // Step 2
  performance.mark('step2-start');
  await page.fill('input[name="email"]', 'test@example.com');
  await page.click('[data-testid="next"]');
  await performance.waitUntilStable();
  performance.mark('step2-end');
 
  // Step 3
  performance.mark('step3-start');
  await page.click('[data-testid="submit"]');
  await performance.waitUntilStable();
  performance.mark('step3-end');
 
  // Create measures
  const step1 = performance.measure('step-1', 'step1-start', 'step1-end');
  const step2 = performance.measure('step-2', 'step2-start', 'step2-end');
  const step3 = performance.measure('step-3', 'step3-start', 'step3-end');
 
  console.log('Step timings:', { step1, step2, step3 });
});

Retrieving All Metrics

Use getCustomMetrics() to get all recorded marks and measures:

const metrics = performance.getCustomMetrics();
 
console.log('Marks:', metrics.marks);
// [
//   { name: 'step1-start', timestamp: 1234567890 },
//   { name: 'step1-end', timestamp: 1234567990 },
//   ...
// ]
 
console.log('Measures:', metrics.measures);
// [
//   { name: 'step-1', duration: 100, startMark: 'step1-start', endMark: 'step1-end' },
//   ...
// ]

Automatic Artifact Inclusion

Custom metrics are automatically included in test artifacts:

{
  "testName": "wizard flow",
  "customMetrics": {
    "marks": [
      { "name": "step1-start", "timestamp": 1234567890 },
      { "name": "step1-end", "timestamp": 1234567990 }
    ],
    "measures": [
      { "name": "step-1", "duration": 100, "startMark": "step1-start", "endMark": "step1-end" }
    ]
  }
}

Clearing Metrics

When using reset(), custom metrics are also cleared:

await performance.init();
 
performance.mark('initial');
await performance.reset(); // Clears all marks and measures
 
performance.mark('after-reset');
const metrics = performance.getCustomMetrics();
// Only contains 'after-reset', not 'initial'

Practical Examples

Time to Interactive

performance.mark('navigation-start');
await page.goto('/');
performance.mark('dom-loaded');
 
await page.waitForSelector('[data-testid="interactive-element"]');
performance.mark('interactive');
 
const loadTime = performance.measure('load-time', 'navigation-start', 'dom-loaded');
const tti = performance.measure('time-to-interactive', 'navigation-start', 'interactive');
 
console.log(`Load: ${loadTime}ms, TTI: ${tti}ms`);

Animation Timing

performance.mark('animation-start');
await page.click('[data-testid="open-modal"]');
await page.waitForSelector('.modal.visible');
performance.mark('animation-end');
 
const animationTime = performance.measure('modal-animation', 'animation-start', 'animation-end');
console.log(`Modal animation: ${animationTime}ms`);

Form Validation

performance.mark('validation-start');
await page.fill('input[name="email"]', 'invalid');
await page.click('[type="submit"]');
await page.waitForSelector('.error-message');
performance.mark('validation-end');
 
const validationTime = performance.measure('form-validation', 'validation-start', 'validation-end');
console.log(`Validation feedback: ${validationTime}ms`);

Next Steps