javascript

Measuring Node.js Performance in Production with Performance Hooks

Damilola Olatunji

Damilola Olatunji on

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:

javascript
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"] });

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:

javascript
console.log(PerformanceObserver.supportedEntryTypes);

This will display a variety of entry types:

javascript
[ "dns", "function", "gc", "http", "http2", "mark", "measure", "net", "resource", ];

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

javascript
observer.observe({ entryTypes: PerformanceObserver.supportedEntryTypes });

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:

shell
git clone https://github.com/damilolaolatunji/nodejs-perf

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

shell
cd nodejs-perf npm install

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:

javascript
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}`); });

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:

shell
npm run start-dev

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

shell
curl http://localhost:3000

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:

javascript
// 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'] }); . . .

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:

shell
curl http://localhost:3000

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

javascript
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 }

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

javascript
if (entry.initiatorType === "fetch") { console.log(entry.name, entry.duration); }

Resulting in:

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

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:

text
// .env APPSIGNAL_PUSH_API_KEY=<your push api key>

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

javascript
// 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, }); . . .

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

javascript
// 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'] }); . . .

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:

shell
while true; do curl http://localhost:4000; sleep 5; done

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:

text
%name% %field% %tag%
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.

Damilola Olatunji

Damilola Olatunji

Damilola is a freelance technical writer and software developer based in Lagos, Nigeria. He specializes in JavaScript and Node.js, and aims to deliver concise and practical articles for developers. When not writing or coding, he enjoys reading, playing games, and traveling.

All articles by Damilola Olatunji

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps