As someone who has spent over eight years working with Node.js in production environments, I've had my fair share of ups and downs with its performance. While we had a great run together, it's no longer 2015, and there are now arguably much better options for backend development, such as .NET, Go, or even Rust. Nonetheless, if you're using Node.js for your projects, I'd like to share some performance tips I've learned along the way, which might help you optimize your applications. Before diving in, I'd like to acknowledge the incredible work of Matteo Collina, who has made a massive impact on the Node.js community through his numerous libraries.
Opt for Asynchronous Logging
Logging can be a performance killer, as it happens synchronously on the event loop by default. If you're outputting a lot of logs (or even if you aren't), consider switching to Pino, an asynchronous logger:
https://github.com/pinojs/pino
Be cautious with logging calls in tight loops or hot functions, as they can harm performance even if you're not actually outputting logs (i.e. when LOG_LEVEL=warn
, some libraries will still cause significant overhead for trace/debug/info logs). Additionally, avoid using JSON.stringify in your logging code, as you'll pay for that even if you're not logging it out.
Increase UV_THREADPOOL_SIZE for Better IO Performance
If your application relies heavily on DNS, cryptography, or IO operations, try increasing the UV_THREADPOOL_SIZE. By default, it's set to 4, which might not be sufficient for your needs:
https://nodejs.org/api/cli.html#uv_threadpool_sizesize
Node.js doesn't have built-in DNS caching and relies on libuv threads, which can cause queuing issues in a microservices architecture. To address this, consider using a library like cacheable-lookup, which provides an easy way to implement DNS caching:
https://www.npmjs.com/package/cacheable-lookup
Be Wary of APM Clients
One of my biggest frustrations with Node.js is the lack of efficient APM (Application Performance Monitoring) options. Most APM clients can severely degrade performance, even up to 90% or more. This is especially true for services like NewRelic, Datadog, Dynatrace, and Elastic APM. N|Solid is another option that relies on forking and instrumenting V8, but I haven't personally tried it. Check out this benchmark:
https://benchmark.nodesource.com/
Monitor Node.js Microservices with Pixie
If you're working with Node.js microservices, particularly in a Kubernetes environment, Pixie is another powerful tool that can help you monitor and observe your application's performance. Pixie, developed by Pixie Labs, is an observability platform that uses eBPF (Extended Berkeley Packet Filter) at the Linux kernel level to collect data about your applications with minimal performance impact.
Some benefits of using Pixie for your Node.js microservices include:
- No code changes required: Pixie automatically instruments your applications, eliminating the need to modify your code for monitoring purposes.
- Minimal performance impact: Since Pixie operates at the kernel level using eBPF, it adds negligible overhead to your application, allowing you to monitor performance without sacrificing efficiency.
- Instant observability: Pixie provides instant, high-level insights into your microservices' performance, such as latency, error rates, and resource usage, without the need for complex configurations or setups.
While Pixie doesn't offer the same depth of APM insights as some other tools, it's an excellent option for developers who need a lightweight, hassle-free solution for monitoring Node.js microservices in Kubernetes environments. You can learn more about Pixie and how to get started on their official website:
You can also use Pixie via NewRelic:
https://newrelic.com/platform/kubernetes-pixie
Optimize Your Web Applications with Fastify and Mercurius
When developing web applications with Node.js, choosing the right framework and libraries can make a significant impact on performance. Two such libraries worth considering are Fastify and Mercurius, both of which have been developed with performance in mind.
Fastify
Fastify is a high-performance web framework for Node.js designed to be both fast and lightweight. Its core features include a powerful plugin system, a schema-based approach to request validation and response serialization, and built-in support for HTTP/2 and async/await. By using Fastify, you can reduce the overhead of your web server, which in turn can lead to improved response times and overall application performance.
You can learn more about Fastify and how to get started on their official website:
Mercurius
Mercurius is a high-performance GraphQL library for Fastify, designed to provide an optimized and streamlined GraphQL experience. It supports features like schema stitching, persisted queries, and JIT (Just-In-Time) compilation, enabling you to build efficient and scalable GraphQL APIs with ease. By using Mercurius in conjunction with Fastify, you can create powerful and performant web applications that leverage the flexibility and expressiveness of GraphQL.
You can learn more about Mercurius and how to integrate it with Fastify on their GitHub repository:
https://github.com/mercurius-js/mercurius
Leverage Clinic.js for Performance Profiling
Another valuable tool to help you optimize your Node.js applications is Clinic.js, a suite of utilities created by NearForm. Clinic.js includes several performance profiling tools that can assist you in pinpointing bottlenecks and performance issues within your code:
- Doctor: Helps you diagnose your application by analyzing its health, identifying potential issues related to CPU usage, memory leaks, and event loop delays.
- Bubbleprof: Visualizes your application's asynchronous activity, making it easier to identify the bottlenecks in your code related to I/O, timers, and promises.
- Flame: Generates flame graphs to help you understand the CPU usage of your application, allowing you to pinpoint functions that consume excessive processing time.
You can learn more about Clinic.js and how to use it on their official website:
Conclusion
While Node.js might not be my go-to choice for backend development in 2023, it's still a viable option for many projects. By following the tips outlined in this article, you can optimize your Node.js applications and squeeze more performance out of your existing codebase. If you're starting a new project, in my opinion it's worth exploring alternative backend technologies like .NET, Go, Java (or anything on the JVM) or Rust to see if they might be a better fit for your needs - it all depends on the use case!