Anshul Goyal I love to code and use new technologies.

Improving Node.js performance using Rust

8 min read 2337

Improving Node.js Performance Using Rust

Editor’s note: This article was last updated on 6 July 2023 to better explore the popular methods to leverage Rust’s features in a Node.js application, including Neon and the Foreign Function Interface (FFI).

Node.js is a popular JavaScript runtime for writing backend applications. Its flexibility and nonblocking nature have made it the premier choice for API consumption.

JavaScript, as a scripting language, might not be as fast as some compiled languages. However, thanks to optimizations from the V8 engine, it performs sufficiently well for most practical applications. It’s important to note that Node.js, which is single-threaded, isn’t ideal for heavy computational tasks as blocking the main thread with lengthy calculations can lead to performance issues. Fortunately, Node.js supports worker threads, providing a way to handle such intensive tasks without blocking the main thread.

Even with the benefits of worker threads, JavaScript can still be slower compared to other languages. Luckily, we can use Rust’s horsepower to improve the performance of our Node.js application. There are two popular ways you can leverage Rust to boost your Node.js performance:

In the course of this article, we’ll learn how to use these tools to build high-performance Node.js applications. A prerequisite to follow along with this article is a basic understanding of both Rust and Node.js.

Jump ahead:

How do Rust and Node.js work together?

Rust is a systems programming language developed by Mozilla. It’s known for its speed and its ability to handle tasks concurrently. One of its notable features is its ability to natively use C functions and libraries, and also export its functions to be used in C, a capability that significantly broadens its utility and compatibility.

This interoperability with C is part of Rust’s broader design philosophy, which provides developers with low-level control and high-level ergonomics — it gives you control of memory management without the hassle associated with these controls. It also delivers zero-cost abstraction, so you only pay for what you use.

Similar to Rust interoperability, Node.js native addons allow you to write C++ (or C) code that is dynamically linked into the Node.js runtime and can be called from JavaScript. This can be used to perform computationally intensive tasks more efficiently, or to interface with system APIs or other libraries that aren’t accessible from JavaScript. You can load them into Node.js using the require() function and use them as if they were ordinary Node.js modules.

There are two types of Node.js addons supported by Node.js. Let’s take a quick look at them now:

  • C++ addons: A C++ addon is an object that can be mounted by Node.js and used in the runtime. Because C++ is a compiled language, these addons are very fast. C++ has a wide array of production-ready libraries that can be used to expand the Node.js ecosystem. Many popular libraries use native addons to improve performance and code quality
  • Node-API C++/C addons: The main problem with C++ addons is that you need to recompile them with every change to the underlying JavaScript runtime. It causes a problem with maintaining the addon. N-API tries to eliminate this by introducing a standard application binary interface (ABI). The C header file remains backward compatible, which means you can use the addon compiled for a particular version of Node.js with any version greater than the version for which it was compiled. You would use this method to implement your addon

Rust can mimic the behavior of a C library. In other words, it exports the function in a format C can understand and use. Rust calls the C function to access and use APIs provided by Node.js. These APIs provide methods for creating JavaScript strings, arrays, numbers, errors, objects, functions, and more.

Note that creating a Node.js addon directly with Rust and Node-API — without using a helper library— is a bit more complex because Node-API is a C API, and interfacing with C from Rust requires careful handling of types and memory.

This means you’re most likely going to write a truckload of unsafe code. In most cases, you’ll use crates like neon or nodejs-sys, which uses N-API under the hood, to make it more flexible. While nodejs-sys is lower-level compared to neon, it gives you more flexibility and ease and if there is an additional performance overhead, it’ll be negligible because Neon prioritizes performance while retaining its flexibility and safety. Unfortunately, Neon does not implement everything in the Node-API, but if you do need to implement something that Neon does not implement, you can use the nodejs-sys crate.

Previously, we mentioned that FFI is also another popular method to leverage Rust to boost your Node.js applications’ performance. But what is FFI and how does it relate?

What is the Foreign Function Interface (FFI)?

The Foreign Function Interface (FFI) is a mechanism that allows code written in one language to call code written in another language. In our case, we want to use code written in Rust in our Node.js project. With FFI, you can write functions in Rust, compile them into a shared library, and then load and call these functions directly from JavaScript. It is pretty straightforward, although FFI is much slower than using N-API.

Now that we’ve gotten the basic concepts out of the way, let’s implement the integration between Rust and Node.js using FFI and then using Node-API (using Neon). But before we proceed, let’s set up our environment to work with Rust and Node.js:

Setting up our Node.js project

For this tutorial, you must have Node.js and Rust installed on your system with Cargo and npm. I would suggest using Rustup to install Rust and nvm for Node.js.

We’ll start by creating a Neon project. Create a directory named rust-addon and initialize a new Neon project by running npm init neon hello-word. This will prompt you to answer a few questions, like your regular npm initialization. However, the result will be a boilerplate for a Node and Rust project. Your directory structure will look like so:

├── Cargo.toml
├── package.json
└── src

No further configuration is needed, so we can start writing our Rust app that will be exported to Node.js.

Writing the Rust code with Neon

Let’s create a simple Rust function, export it as a Node.js module, and then import and use it within a Node.js application. Replace the initial contents in the src/ directory with the code below:

use neon::prelude::*;

fn factorial(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let n = cx.argument::<JsNumber>(0)?.value(&mut cx) as u64;
    let result = (1..=n).product::<u64>();
    Ok(cx.number(result as f64))

fn main(mut cx: ModuleContext) -> NeonResult<()> {
    cx.export_function("factorial", factorial)?;

In the above code, we are mimicking a computationally heavy task using factorial as an example. Let’s break down every part of it.

In the factorial function argument, you’ll notice the FunctionContext type. It provides you with methods for interacting with the JavaScript runtime, such as accessing function arguments, creating new JavaScript values, and throwing exceptions. In our example, we’ve used it to allow JS to accept a number as an argument — the number we want to calculate its factorial:

let n = cx.argument::(0)?.value(&mut cx) as u64;

In the code snippet, cx.argument::(0)? retrieves the first argument passed to the factorial function and converts it to a JavaScript number. Then, let result = (1..=n).product::(); calculates the factorial of the number. Finally, cx.number(result as f64) returns the result as a JavaScript number by converting the Rust f64 number to a JavaScript number using the FunctionContext.number method.

Then in the main function, we add a macro to mark it as the main Neon entry point, and then export the function. Whatever name you give the function, here is what will be used to call it in your Node.js code:

cx.export_function("factorial", factorial)?;

Next, run the build command, npm run build, to compile the code. When that compilation is complete, an index.node file will be generated, and the file will contain the machine code for the factorial function you wrote in Rust, along with information about how to call this function from JavaScript.

Finally, create an app.js file, require the addon index.node, and then run the JavaScript code:

const addon = require('./index.node');

The result should look like this:

Requiring The Index.node Addon In The App.js File

That’s all there is to it. You’ve successfully written a Rust code and used it in your JavaScript code. Note that this is a simple example, and it can get much more complex than this. Refer to the Neon documentation if you get stuck.

Next, let’s take a look at how you can achieve the same thing with FFI.

Writing the Rust code with FFI

As we’ve already established, FFI is one of the popular methods we can use to leverage Rust’s powerful features in our Node.js applications. Let’s quickly set up a new project for FFI implementation because the implementation differs.

Run the code below in a different directory to initialize a new Rust project:

cargo init --lib

The command above should generate a directory structure similar to the one we initially had but without the JavaScript package.json file:

├── Cargo.toml
└── src

So, we’ll write the factorial code in the src/ file:

pub extern "C" fn factorial(n: u64) -> u64 {

The code above is a conventional Rust code; we just added the #[no_magle] attribute and the extern "C" function signature. So, what is the no_mangle?

In compiler construction, name mangling (also called name decoration) is a technique used to solve various problems caused by the need to resolve unique names for programming entities in many modern programming languages. It provides a way of encoding additional information in the name of a function, structure, class, or another datatype in order to pass more semantic information from the compiler to the linker.


What about the extern "C"?

The extern "C" keyword is used to specify that a function should use the C language’s Application Binary Interface (ABI), which dictates how functions are called and data is represented. This is crucial when creating functions in Rust that will be called from other languages, like Node.js, as it ensures compatibility at the binary level.

With that, we have our Rust application ready. Next, we need to convert it to a dynamic library to use it in our Node.js application.

Before we proceed, we need to update the Cargo.toml file to configure the Rust project as a dynamic library. So, your Cargo.toml file should look like this:

name = "rust-ffi"
version = "0.1.0"
edition = "2021"

path = "src/"
crate-type = ["dylib"]

Pay special attention to the [lib] section. Notice that we are specifying the name, path, and crate-type. These parameters are required to create a dynamic library, so make sure you specify them. The crate-type will instruct the compiler to generate the library with a file extension of .so on Linux, .dll on Windows, or .dylib on macOS.

Finally, to finish up on the Rust side of things, let’s create the build by running the code below:

cargo build --release

After running the above command, the dynamic library will be generated in the target directory as shown below:

Generating The Dynamic Library

Now is the time we want to use the library in our Node.js project. Start by setting up a Node.js project using the command npm init -y. Next, create a file named app.js. Finally, install ffi-napi by running npm install ffi-napi. ffi-napi is a Node.js library that enables you to call dynamic libraries directly from your JavaScript code.

In your app.js file, add the following code:

const ffi = require('ffi-napi');

// Define the argument types and return type of the factorial function
const { factorial } = ffi.Library('./librustffi.dylib', {
  'factorial': ['uint64', ['uint64']],

Let’s understand what’s going on in this example. First, we import the FFI library and then use it to link the dynamic library we created from Rust.

The FFI library takes two parameters, the first parameter is the path to the dynamic library while the second parameter defines the function signature. So, from our example, we are looking for the factorial function that accepts an integer and returns an integer. Then finally, we call the function passing 20 as the parameter.

If we run the code, we’ll get the following result:

Importing The FFI Library

If you double check both results, you’ll notice that they are the same. You might not be able to notice the performance difference between the two implementations until you use them in a more complex calculation scenario.

Final words

This guide merely scratches the surface of the potential that the Rust and Node.js combination holds. We’ve walked through the basics of integrating Rust code into a Node.js application, using both the Neon and Foreign Function Interface methods. These examples should provide a solid foundation for your future projects involving Node.js and Rust.

There are also other libraries based on Node-API that you might find useful, including nodejs-sys, node-bindgen, and napi-rs. Feel free to explore these further. And if you want to learn more about Nodejs-API, check out the documentation.

Happy hacking!

200’s only Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket. LogRocket Network Request Monitoring

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Anshul Goyal I love to code and use new technologies.

6 Replies to “Improving Node.js performance using Rust”

  1. > Rust can mimic the behavior of a C library.
    Like C++ can’t? 🙂
    What `extern “C”` is for in C++ then? 😉

  2. Rust is somehow flew above my head. However node.js is something that I use everyday in my projects. It help me doing a lot of things easily. For example, I use gulp for automating tasks.

  3. What is meant by “You can use WebAssembly to create a node_module, but all Node.js functionality is not available”? Can’t you create a wasm and bind to any part of the Node.js API with wasm-bindgen?

Leave a Reply