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:
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:
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?
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:
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 ├── README.md ├── package.json └── src └── lib.rs
No further configuration is needed, so we can start writing our Rust app that will be exported to Node.js.
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/lib.rs
directory with the code below:
//src/lib.rs 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)) } #[neon::main] fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("factorial", factorial)?; Ok(()) }
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'); console.log(addon.factorial(10));
The result should look like this:
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.
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 └── lib.rs
So, we’ll write the factorial code in the src/lib.rs
file:
#[no_mangle] pub extern "C" fn factorial(n: u64) -> u64 { (1..=n).product() }
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:
[package] name = "rust-ffi" version = "0.1.0" edition = "2021" [dependencies] [lib] name="rustffi" path = "src/lib.rs" 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:
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']], }); console.log(factorial(20));
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:
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.
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!
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 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.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
8 Replies to "Improving Node.js performance using Rust"
> Rust can mimic the behavior of a C library.
Like C++ can’t? 🙂
What `extern “C”` is for in C++ then? 😉
very well written post!
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.
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?
can you eloborate on that.
what about connecting the two using rpc?
Can we please stop writting article with AI. I could tell with the first sentance and then ran it through an AI detector, and guess what AI generated.
Hi Tom, thanks for your feedback. The first sentence of this article is the same as when this article was first published in 2020, before ChatGPT’s first release. It’s possible a different AI content generator was used, but we’ve found that certain writing styles tend to get marked as AI-generated when they’re not, which is the more likely explanation here. While things can certainly slip through the cracks, our team rigorously checks every draft and update we receive for both plagiarism and AI usage, and we have worked hard to ensure that the authors we work with write original content with integrity using their personal experience as developers. Still, we recognize it can be frustrating to read an article that sounds like AI, and we appreciate your reaching out to let us know your thoughts!