Node.js Perfomance Hooks


1. Introduction

Performance measurement is a vital part of a successful application. To provide the best customer experience, we need to have a website that’s as fast as possible.

A failure to provide a well-performing application to our customers could result in losing a significant amount of revenue. For example, if every page in Amazon would load just a second lower, the company will lose $1.6 billion per year 1. Besides, Google uses page speed as an essential attribute when ranking pages 2. Hence, to be able to optimize the performance of our application, we need to start monitoring it from day 1.

In this article, we’re going to look into the Node’s performance measurement API, which helps us monitor the performance in a convenient way.

2. The classical problems with performance monitoring

If we would like to see how long it takes to execute a piece of code, the first tool that might pop into our mind might be Date. For example, we could do something like this:

const someLongRunningOperation = () => {
    return new Promise(resolve => {
        setTimeout(resolve, 2000)
    });
}

const calculateSomething = async () => {
    const start = new Date();
    await someLongRunningOperation();
    const end = new Date();
    console.log(`It took ${end - start} ms to execute the function`);
}

calculateSomething();

This code gets the current timestamp before executing someLongRunningOperation and subtract the current timestamp after finishing with someLongRunningOperation. On first sight, the solution works, but it introduces two problems.

First, the Date API is not absolute. For example, the value of new Date() could easily change to the past if we happen to switch to the summertime in Europe.

Also, adding the measurement logic directly to our codebase should be avoided, if possible. The additional code clutters the codebase and makes it significantly harder to understand the primary purpose of the application.

3. Performance Hooks basics

Node Performance Hooks provide functionality which helps us with two issues mentioned in the previous paragraph. First, performance hooks provide us with a way to get guaranteed absolute measurements. Secondly, performance hooks help us get rid of the measurement in our critical codebase.

Let’s take a look at the exact implementation:

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

const obs = new PerformanceObserver((items) => {
    console.log(items.getEntries()[0].duration);
    performance.clearMarks();
});

obs.observe({ entryTypes: ['measure'] });

const someLongRunningOperation = () => {
    return new Promise(resolve => {
        setTimeout(resolve, 2000)
    });
}

const calculateSomething = async () => {
    performance.mark('start');
    await someLongRunningOperation();
    performance.mark('end');
    performance.measure('Start to end', 'start', 'end');
}

calculateSomething();

As we can see, we need to require PerformanceObserver and performance. The first one is an observer, which will be responsible for providing notifications when performance entries are added to the performance timeline.

What is a performance timeline, after all? That’s the place where performance comes to play - it allows us to set specific events on the timeline using methods mark and measure. For example, we set two entries to the timeline using mark - one called “start” and the other called “end”. Later we use measure to add a measurement entry to the timeline.

To listen for the entries on the performance timeline, we first need to create a new instance of PerformanceObserver by passing a callback which is invoked once the correct entries will be present in the timeline. After we have the observer, we need to make it listen to the specific timeline entries using observe, which allows us to specify which type of entries we would like to be notified on. The possible options in Node are mark, measure or function.

Inside the callback, we can determine what we’d like to do with the timeline entry. For example, we could send the metric to Datadog, or publish it so Prometheus could read it.

4. Advanced Performance Hooks

Once we’ve grasped the basics of performance hooks, we can now look into the more advanced use cases.

If we’d like to get rid of any performance measurement code, we can do it using performance.timerify. timerify wraps a function with duration measurement logic. Once we invoke the function, then a PerformanceObserver which is subscribed to timeline entry type function gets notified. Let’s take a closer look:

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

const obs = new PerformanceObserver((items) => {
    console.log(items.getEntries()[0].duration);
    performance.clearMarks();
});

obs.observe({ entryTypes: ['function'] });

const someLongRunningOperation = () => {
    for(i = 0; i < 1_000_000_000; i++) {

    }
}

const timedLongRunningOperation = performance.timerify(someLongRunningOperation);

const calculateSomething = () => {
    timedLongRunningOperation();
}

calculateSomething();

We create a function someLongRunningOperation which is wrapped by timerify. Once we execute the wrapped function called timedLongRunningOperation, then the callback provided to the PerformanceObserver constructor gets executed.

5. Conclusion

In this article we briefly looked into Node’s Performance Hooks. They provide us a convenient way to measure the performance of our application without writing any measurment-related code directly to our codebase.