DEV Community

Cover image for Measuring Node.js Performance in Production with Performance Hooks
Damilola Olatunji for AppSignal

Posted on • Originally published at blog.appsignal.com

Measuring Node.js Performance in Production with Performance Hooks

In the first part of this series, we toured performance APIs in Node.js. We discussed the capabilities of APIs and how they can diagnose slowdowns or network issues in your Node application.

Now, in this concluding segment, we will embark on a practical journey, applying these performance hooks in a real-world scenario. You will understand how to effectively use these tools to monitor and enhance your application's performance.

Let's dive in and see these concepts in action!

Prerequisites

Before proceeding with this tutorial, ensure you have a foundational understanding of the Node.js Performance Measurement APIs as outlined in part one of this series.

Continuous Monitoring with Performance Observer for Node

Continuing our exploration of Node.js Performance APIs, we come to the Performance Observer. This API allows you to track new PerformanceEntry instances in real time as they are added to the Performance Timeline. Let's examine how to implement it:

import { performance, PerformanceObserver } from "node:perf_hooks";

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry);
  }
});

observer.observe({ entryTypes: ["resource"] });
Enter fullscreen mode Exit fullscreen mode

With this configuration, every time a PerformanceEntry with the 'resource' entryType is added to the Performance Timeline, it gets logged automatically to the console. To explore all the entry types available for monitoring, you can execute:

console.log(PerformanceObserver.supportedEntryTypes);
Enter fullscreen mode Exit fullscreen mode

This will display a variety of entry types:

[
  "dns",
  "function",
  "gc",
  "http",
  "http2",
  "mark",
  "measure",
  "net",
  "resource",
];
Enter fullscreen mode Exit fullscreen mode

To broaden the scope of monitoring to encompass all these entry types, adjust your observer like so:

observer.observe({ entryTypes: PerformanceObserver.supportedEntryTypes });
Enter fullscreen mode Exit fullscreen mode

A use case for observing the performance timeline is to upload performance measurements to an Application Performance Monitoring (APM) service like AppSignal. This way, you can keep track of how the key metrics you care about are performing and get alerted to any performance degradation.

In the following sections, I'll present a practical example showcasing the use of the Performance Observer in real-world application contexts.

A Practical Example: Monitoring Third-Party API Integrations for Node

To maintain a seamless user experience within your app, you need to track the performance of the external APIs you rely on. Delays in API responses can lead to slower interactions, adversely affecting your application's responsiveness.

To mitigate this, it's advisable to choose API providers that offer response time guarantees. Additionally, you can implement a strategy to switch to a fallback API or use cached resources when experiencing latency spikes.

Let's explore a practical scenario where your Node.js application interacts with multiple third-party APIs. You need to track each request's latency to establish a performance baseline and monitor adherence to Service-Level Agreements (SLAs).

In such cases, you can use the performance measurement APIs to collect the timing data, format it appropriately, and dispatch it periodically to a performance monitoring tool or a logging system.

This ongoing process allows you to understand the typical performance parameters for each API, so you can quickly identify and resolve any anomalies or unexpected delays in API responses.

In the following example, you'll set up monitoring for a Node.js application's API requests using AppSignal. This setup ensures that you can always track an API's response times and get alerted to performance issues automatically.

Let's begin!

Setting up the Demo Repository

To follow through with the example below, clone this repository to your computer with the command below:

git clone https://github.com/damilolaolatunji/nodejs-perf
Enter fullscreen mode Exit fullscreen mode

After cloning, switch to the repository's directory and install the necessary dependencies:

cd nodejs-perf
npm install
Enter fullscreen mode Exit fullscreen mode

Then, open the project in your text editor and navigate to the server.js file. This file includes a basic Fastify server setup with one route:

import Fastify from "fastify";

const fastify = Fastify({
  logger: true,
  disableRequestLogging: true,
});

fastify.get("/", async (_request, reply) => {
  const res1 = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  await res1.json();

  const res2 = await fetch("https://covid-api.com/api/reports/total");
  await res2.json();

  const res3 = await fetch("https://ipinfo.io/json");
  await res3.json();
});

const port = process.env.PORT || 3000;

fastify.listen({ port }, (err, address) => {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }

  fastify.log.info(`Fastify is listening on port: ${address}`);
});
Enter fullscreen mode Exit fullscreen mode

This route performs three GET requests and returns a 200 OK response. While this is a simplistic example, it's perfectly suited to demonstrate how performance hooks can be employed in real-world applications.

To start the server for development on port 3000, run:

npm run start-dev
Enter fullscreen mode Exit fullscreen mode

Verify that the server is functioning correctly by sending a request to the root using curl. The command should execute successfully:

curl http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

In the following section, you'll use the PerformanceObserver API to monitor each API request continuously.

Tracking the API Requests

Since the Fetch API automatically adds resource timing data to the Performance Timeline for each request, you can automatically track the measurements using the PerformanceObserver API as shown below:

// server.js
import Fastify from 'fastify';
import { performance, PerformanceObserver } from 'node:perf_hooks';

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.initiatorType === 'fetch') {
      console.log(entry);
    }
  }

  performance.clearResourceTimings();
});

observer.observe({ entryTypes: ['resource'] });

. . .
Enter fullscreen mode Exit fullscreen mode

In this setup, each time a PerformanceResourceTiming object is added to the timeline, the script checks if the initiator is a fetch request and logs the details to the console. Clearing the resource timings after logging ensures room for new entries.

Once you update the file, the server should restart, and you can repeat the curl command from earlier:

curl http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

The server's console should now display three PerformanceResourceTiming entries sequentially:

PerformanceResourceTiming {
  name: 'https://jsonplaceholder.typicode.com/posts/1',
  entryType: 'resource',
  startTime: 205250.912353009,
  duration: 2046.0582769811153,
  initiatorType: 'fetch',
  nextHopProtocol: undefined,
  workerStart: 0,
  redirectStart: 0,
  redirectEnd: 0,
  fetchStart: 205250.912353009,
  domainLookupStart: undefined,
  domainLookupEnd: undefined,
  connectStart: undefined,
  connectEnd: undefined,
  secureConnectionStart: undefined,
  requestStart: 0,
  responseStart: 0,
  responseEnd: 207296.9706299901,
  transferSize: 300,
  encodedBodySize: 0,
  decodedBodySize: 0
}
PerformanceResourceTiming {
  name: 'https://covid-api.com/api/reports/total',
  entryType: 'resource',
  startTime: 207298.71959400177,
  duration: 4112.256608009338,
  initiatorType: 'fetch',
  nextHopProtocol: undefined,
  workerStart: 0,
  redirectStart: 0,
  redirectEnd: 0,
  fetchStart: 207298.71959400177,
  domainLookupStart: undefined,
  domainLookupEnd: undefined,
  connectStart: undefined,
  connectEnd: undefined,
  secureConnectionStart: undefined,
  requestStart: 0,
  responseStart: 0,
  responseEnd: 211410.9762020111,
  transferSize: 300,
  encodedBodySize: 0,
  decodedBodySize: 0
}
PerformanceResourceTiming {
  name: 'https://ipinfo.io/json',
  entryType: 'resource',
  startTime: 211411.61356300116,
  duration: 2160.16487801075,
  initiatorType: 'fetch',
  nextHopProtocol: undefined,
  workerStart: 0,
  redirectStart: 0,
  redirectEnd: 0,
  fetchStart: 211411.61356300116,
  domainLookupStart: undefined,
  domainLookupEnd: undefined,
  connectStart: undefined,
  connectEnd: undefined,
  secureConnectionStart: undefined,
  requestStart: 0,
  responseStart: 0,
  responseEnd: 213571.7784410119,
  transferSize: 300,
  encodedBodySize: 0,
  decodedBodySize: 0
}
Enter fullscreen mode Exit fullscreen mode

For a more focused output, you can modify the logging to include only the relevant details:

if (entry.initiatorType === "fetch") {
  console.log(entry.name, entry.duration);
}
Enter fullscreen mode Exit fullscreen mode

Resulting in:

https://jsonplaceholder.typicode.com/posts/1 1969.7759200036526
Ghttps://covid-api.com/api/reports/total 3156.867073982954
https://ipinfo.io/json 1885.5162260234356
Enter fullscreen mode Exit fullscreen mode

With the ability to track, format, and log each request's timing, the next phase involves integrating these metrics with a monitoring solution like AppSignal.

Before moving to the next section, sign up for a 30-day free trial of AppSignal.

Tracking Node.js Performance Measurements with AppSignal

Once you've signed into your AppSignal account, create a new application and follow the setup instructions. Copy the Push API Key when prompted or head over to the settings and find the key in the Push & Deploy section.

Back in your project, create a .env file at the root directory and add your AppSignal Push API Key:

// .env
APPSIGNAL_PUSH_API_KEY=<your push api key>
Enter fullscreen mode Exit fullscreen mode

Once saved, return to your server.js file and
initialize your AppSignal configuration
as follows:

// server.js
import 'dotenv/config';
import { Appsignal } from '@appsignal/nodejs';
import Fastify from 'fastify';
import { performance, PerformanceObserver } from 'node:perf_hooks';

new Appsignal({
  active: true,
  name: 'Node.js Perf Demo',
  pushApiKey: process.env.APPSIGNAL_PUSH_API_KEY,
});

. . .
Enter fullscreen mode Exit fullscreen mode

Next, modify the PerformanceObserver to start tracking the timing data in AppSignal:

// server.js
. . .
const meter = Appsignal.client.metrics();

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.initiatorType === 'fetch') {
      meter.addDistributionValue('external_api_request', entry.duration, {
        url: entry.name,
      });
    }
  }

  performance.clearResourceTimings();
});

observer.observe({ entryTypes: ['resource'] });
. . .
Enter fullscreen mode Exit fullscreen mode

The addDistributionValue() method is ideal for tracking metrics over time, allowing you to create graphs based on average values or call counts.

Its first argument is the metric name, and the second is the value to be tracked. It also accepts one or more tags in an optional object, which, in this case, is each third-party integration being tracked in the program.

Save the file and wait for the server to restart. Test the setup by sending requests to the / route every five seconds using this script:

while true; do curl http://localhost:4000; sleep 5; done
Enter fullscreen mode Exit fullscreen mode

With the script running, head back to your AppSignal application and click Add dashboard on the left followed by Create a dashboard on the resulting page. Title your dashboard and click Create dashboard.

Once the dashboard is created, click the Add graph button. Give your graph a title (such as "Request durations") and click the add metric button:

Add a graph form

Find the external_api_request metric option and configure the graph as shown below:

Add a metric

Click the Back to overview button, and change the value of the Label of value in Legend field to:

%name% %field% %tag%
Enter fullscreen mode Exit fullscreen mode

Label value in Legend

Once you're done, click the Create graph button on the bottom right. You will now see a line graph plotting the duration of each API request made by your program.

AppSignal Performance Dashboard

You can hover on the line graph to view the mean, 90th, and 95th percentile for each request.

Hovering over the graph

Any spikes or unusual patterns in the chart can be investigated to pinpoint the cause of performance issues.

See the AppSignal documentation for more information on metrics and dashboards.

Getting Alerted to Performance Issues

When you've established the normal behavior of an API, you can set up alerts to detect when anomalies occur. AppSignal simplifies this process, allowing you to quickly configure alerts for unusual performance patterns.

Return to your dashboard and find the Anomaly Detection > Triggers section in the lower left. Click the Add a trigger button on the resulting page:

Set a trigger on any metric

In the New trigger form, click Custom metrics at the bottom of the sidebar. Here, configure the trigger based on your specific requirements. Ensure you select external_api_request as the Metric name and choose an appropriate Comparison operator that suits the alert condition you want to monitor.

Trigger form

To receive email alerts, ensure that the Email notifications option is checked. This will notify you via email whenever the set conditions are met, helping you stay informed about any performance issues in real time. You can also add other alerting integrations like Slack, PagerDuty, Discord, etc., in the application settings.

Once you've set up the trigger conditions and notification settings, click Save trigger. This action will activate the trigger, and AppSignal will start monitoring the specified metric according to the conditions you've set.

Email alert example

And that's it!

Wrapping Up

With this setup in place, you can now track your third-party integrations reliably and get alerted if any performance issues arise.

I hope this has given you an idea of how to use the Node.js performance APIs in tandem with AppSignal to track, monitor, and ultimately improve your Node application's performance.

Cheers, and happy coding!

P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.

Top comments (0)