Node.js Performance Tracking with Mark and Measure

Node.js provides robust, built-in tools for monitoring application performance through the Performance Measurement APIs, specifically using performance.mark() and performance.measure(). This article explores how these APIs work, demonstrating how to pinpoint execution bottlenecks by establishing high-resolution timestamps, calculating the exact duration between specific application events, and retrieving these metrics using performance observers.

Understanding perf_hooks

Node.js exposes its performance measurement capabilities through the native perf_hooks module. This module implements the W3C User Timing Level 2 specification, allowing developers to collect high-resolution time metrics that are consistent with browser-based performance tooling.

To use these APIs, you first need to import the performance object:

const { performance } = require('perf_hooks');

How performance.mark() Works

The performance.mark() method creates a local timestamp in the performance timeline. This timestamp is highly accurate, utilizing DOMHighResTimeStamp which measures time in milliseconds with microsecond precision.

When you call performance.mark('mark-name'), Node.js records the exact time that line of code executed. You can place these marks before and after complex database queries, API calls, or heavy CPU operations to establish boundaries.

performance.mark('start-database-query');
// ... database operation occurs ...
performance.mark('end-database-query');

How performance.measure() Works

Once you have established start and end marks, you can calculate the elapsed time between them using performance.measure().

The performance.measure(measureName, startMark, endMark) method calculates the duration between the two specified marks, creates a new entry in the performance timeline, and stores the result.

performance.measure(
  'Database Query Duration', 
  'start-database-query', 
  'end-database-query'
);

If you omit the start and end marks, performance.measure() will calculate the time elapsed from the very start of the Node.js process to the moment the measure function is called.

Retrieving Performance Metrics

Creating marks and measures is only useful if you can retrieve and analyze the data. Node.js provides two main ways to access these measurements: direct retrieval and the PerformanceObserver API.

The PerformanceObserver class allows you to subscribe to the performance timeline asynchronously. Whenever a new measure is created, the observer’s callback is triggered. This is the most efficient way to collect metrics without blocking the main event loop.

const { performance, PerformanceObserver } = require('perf_hooks');

// Initialize the observer
const obs = new PerformanceObserver((items) => {
  items.getEntries().forEach((entry) => {
    console.log(`Name: ${entry.name}`);
    console.log(`Duration: ${entry.duration.toFixed(4)} ms`);
    console.log(`Start Time: ${entry.startTime}`);
  });
});

// Observe 'measure' entry types
obs.observe({ entryTypes: ['measure'], buffered: true });

// Run the monitored operation
performance.mark('start-process');
setTimeout(() => {
  performance.mark('end-process');
  performance.measure('Total Process Time', 'start-process', 'end-process');
}, 500);

2. Querying the Performance Timeline

Alternatively, you can retrieve entries synchronously using the performance.getEntriesByName() or performance.getEntriesByType() methods.

// Retrieve all measures
const measures = performance.getEntriesByType('measure');

// Retrieve a specific measure by name
const databaseMeasures = performance.getEntriesByName('Database Query Duration');

Clearing the Timeline

To prevent memory leaks in long-running Node.js processes, it is important to clean up the performance timeline once you have processed the metrics. You can clear specific marks and measures using: