Conf42 Rustlang 2022 - Online

🚀 Supercharge your Node.JS with Rust

Video size:

Abstract

Node took the world by storm. It’s probably one of the most popular Backend frameworks today, and for a good reason - it’s easy to develop, it’s very performant, easy to scale and has a huge community around it.

But let me ask you a simply question: How would you generate a PDF report using Node.js? Think about it for a moment and write down your answer. If you’ve answered puppeteer, pdfjs or spinning an AWS Lambda function - you are probably over complicating things.

JavaScript is not great at heavy computations, and the nature of Node.js being an event-loop driven framework, requires you to think carefully before executing a task that can potentially lock the event-loop.

But there are two solutions to introduce high performance into JavaScript and Node.js: Native Modules and WebAssembly. And what better language than the 2-year-in-a-row-most-loved-language-by-stackoverflow than Rust we can use for that? Join me on an exciting journey into high performance Node.js using Rust Native Modules and WebAssembly, and learn when it better to choose each of them.

Summary

  • Dmitry Kudryavtsev talks about how you can supercharge your nodejs and Javascript experience using Rust. We will get into rust code in just a minute.
  • Neon is a library in the toolchain for embedded rust into JavaScript. Neon is a glue layer between the JavaScript world and the rust world. In a minute we will see how we can use it in JavaScript.
  • Webassembly is a portable binary format, and it's a corresponding text format. Webassembly is actually a compilation target for other languages. It's way, way faster, in some instances around 60% faster than the JavaScript solution. Always run your own benchmarks against the real case scenarios.
  • native libraries can be reused in other languages through foreign function interfaces. The same binary can be shared between the web application using WebAssembly, for example, or the native model. It's a but complicated with both of these.
  • The amount of code we need with the neon wrapper is relatively big. Webassembly on the other hand is typed. With the bus and binding and function, all you need to do is just macro the function. Add the macro and you get a webassembly output.
  • You can't access files from webassembly because Webassembly does not have access. If you need to have access to the standard library, your only solution today is to use native modules. For native models it's always important to recompile to the target architecture.
  • Native modules can't be used in the browser because JavaScript has no support for foreign function interface. Webassembly is meant to replace non performant Javascript code. I hope you learned something.

Transcript

This transcript was autogenerated. To make changes, submit a PR.
Hi there, my name is Dmitry Kudryavtsev. I'm a senior software engineer and I'm passionate about two things, JavaScript and Rust. And so today I want to talk two you how you can supercharge your nodejs and Javascript experience using Rust. Wait, what? Javascript? Yes, I know this is a rust conference, and don't worry, we will get into rust code in just a minute. But before that, let's talk a little bit about JavaScript. We can't ignore the fact JavaScript has taken the world by storm. It's the de facto standard language for anything HTML related. If you want to make your web pages dynamic. JavaScript is the only language you can use AWS of today, and thanks to frameworks like nodeJs, it's becoming increasingly popular in the backend as well. And there are even tools like Electron and Tauri, which is written in rust by the way, that allows you to make so called native desktop applications. So yes, JavaScript is very popular, and whether you like it or not, it's probably here to stay. So aws, you know, as they say, if you can win them, join them. So JavaScript and Node Js are great, but they are sometimes slow, for example if you want to do like cpu bound tasks, maybe image processing or 3d rendering. And luckily there are ways to speed up JavaScript, especially node JS, with the use of native modules, which are written mainly in C or C plus plus or rust. Now you probably want to ask, why do we want to use rust? So let's do a little comparison between Rust and C. Well, C and C, they are very mature, they're very old languages and they're showing their edge, so we can't ignore this fact. Also they lack any modern tooling, so you don't have decent dependency manager has a relatively poor standard library. From my memory you almost always need to include third party libraries like boost if you want to get fancy iterators or collections, maybe changing the new C plus plus versions. I don't follow it anymore. And the biggest problem with those languages is that they are not memory safe. So you get into Sec votes a lot. You probably saw this one, but why rust then? Well it's strongly typed and compiled language, so it has the same performance as a native C or C has a rich standard library, smart pointers, containers, iterators, mutexes, everything you want for it's there has modern tooling, so you have cargo for dependency management and task execution. And the most important thing is that it's memory safe, so you don't get any SeC votes. I know youve can write unsafe rust as well, but let's stay in the safe world of rust. Youve say great, but how do we do that? So we will look at two different approaches and we will compare them. And the first approach that I want to start with is writing native models, especially for nodejs. So meet Neon. Neon is a library in the toolchain for embedded rust into JavaScript. And so we will take a look now at a Fibonacci function that we can write in pure rust and see how we can use it from JavaScript. Below is the code. Don't get overwhelmed, we will go over the code just in a minute. So first we have our requires like we always do. This is all the code that we need from neon. You need to understand the row of neon in this neon is more like a glue layer between the JavaScript world and the rust world. So we have all the different types like js numbers and js strings maybe. So neon provides all this as well. Aws the context for function execution and model execution. Then we have our Fibonacci logic, simple recursive fibonacci function, nothing special in here. And then we have the glue layer. The glue layer is responsible for converting JavaScript and rust and vice versa. So if you have like JavaScript number, which is just a number, and you want to convert it maybe to an Int 32 bit or a float. So this is the conversion layer. When you get a result from rust and you want to pass it back into JavaScript, this is the glue layer. This is what Neon is doing. And this is the most important part between connecting the JavaScript and the rust worlds together. And then we have the main function. Like all executables we have a main function. The main function is responsible into exporting all the functions that we want to access from the JavaScript world. So in this case we are executing the Fibonacci API function and we are naming it as Fibonacci Rs in our JavaScript world. And in just in a minute we will see how we actually use it in JavaScript. Before importing into JavaScript, we obviously need to compile rust code into the native node modules. The neon team maintains a package called Cargo CP Artifact, which essentially once you run this command it will produce a dynamic library. So if you're familiar with dlls from the Windows world or so files from Unix world. So this is essentially what it produces, but it wraps it into the node API so that it will be accessible from nodejs. And so in order to access this code from node js. Don't worry, the code is pretty readable. If you don't know JavaScript, we just require the Fibonacci Rs from the previous file that we've compiled, and we simply execute it as a regular function. So this is one way we can incorporate rust as a native extension into the Nodejs world. Now some of you who probably worked with maybe JavaScript or high performance JavaScript, you probably heard about WaSm. For those of you who didn't heard about WaSm, let's talk a little bit about what is Wasm, which is an abbreviation for webassembly. So webassembly is a portable binary format, and it's a corresponding text format. You can think of it as the assembly language. We have the text format which is the assembly that you write, and the binary format which is converted into the specific architecture of your machine, be it ex 80, 86 or arm whatever youve running on, it's executing by a virtual machine. So there is a VM that's doing the translation between the wasm binary into the actual architecture. It's supported in all major browsers and nodejs, so the VM is implemented in all the major browsers as of today, except for Internet Explorer. And in node js it can be written in a special language called assembly script. If you're familiar with typescript, it's a bit similar to the assembly script is a bit similar to JavaScript, but there are some differences because webassembly is actually typed. But the biggest pro, in my opinion, is that Webassembly is actually a compilation target for other languages. So we can take other languages and rust among them and actually compile them into webassembly. In order to do that, we have another crate called Wassenbindgan. And let's now look at an example how we can compile a Fibonacci function using webassembly. It's way simpler than the neon example, so we only have one macro that is responsible for converting the code into the webassembly. It's then compiled with the special output target as webAssembly. So you get a native webassembly that you can then load into browser or nodejs. Now when we have two or more different tools, the obvious question that you probably ask is that okay, but what about performance? So let's take a look at performant. Don't be overwhelmed by the table and we will go over the numbers here. As you can see, I've run benchmark with the two code hyperfine and I try to compute different Fibonacci numbers. The thing with recursive Fibonacci is that the higher you go with the numbers, the more intensive the computation becomes, because it's a recursive function and it relies on the previous computations. So you can see that the 30th Fibonacci number is relatively fast in all the tree. Actually, you can see that JavaScript is managing pretty good, although native rust is the fastest and wasn't being the second. The changes are not that important. By the way, the green numbers you can see is the performance increase you get from the base nodejs. But then as we look at higher numbers such as the 44th or the 35th Fibonacci numbers, we can see that JavaScript is becoming two struggle really much. While native rust is the performance is increasing, the latency, the time that it takes to compute is increasing as well. But you can see that also it's way, way faster, in some instances around 60% faster than the JavaScript solution. And same can be said about the webassembly version which was compiled from Rust. We can see that it's also faster, it's slower than the native rust, but it's still way faster than the JavaScript. And so if we analyze the data, we can come to two solutions. Number one is that rust increases the performance roughly by 60% compared to node js, while webassembly increases the performance for around 45% compared to the JavaScript. The second conclusion is that rust is around 45% faster than webAssembly, which should not be a surprise because webassembly is executed by a virtual machine in the end. And there is also a third conclusion is that benchmarks like this are useless. They are made to demonstrate a point, but you should always run youve own benchmarks against the real case scenarios. So it's good as a reference point, but don't rely on it when you are making a decision, because as you saw in the example, if you don't go up too much in the Fibonacci numbers, the performance increase you gain might not be worth the hassle in introducing native models or even webAssembly, because as you can see, the forter Fibonacci number is computed relatively fast, even in JavaScript itself. So always run your own benchmarks. Let's do a little comparison. So when to choose native models, when to choose webassembly there are some nuances and let's cover them. The first point I want to touch is if youve talking about maximizing every output, getting the best performance you can get. Youve should absolutely choose native models. It's no surprise native will always be faster than the VM you can see. Look at Java versus C of C Plus Plus. C and C Plus plus will always be faster because they compile natively even though the Java VM is very optimized. Very good. If you want to squeeze every possible performance, then you should go to the native solution as well. Having said that, I want to point out that webassembly is relatively fast, so if you don't need the absolute best performance, consider webassembly. It's a good middle ground between going fully native versus rewriting some of the application parts into webassembly. Let's talk about reusability when we talk about reusability when I talk about reusability, I mean taking your code and using it in a different environment. It's a but complicated with both of these. Let's try to unwrap it and see where the complication comes. Now, native libraries can be reused in other languages through foreign function interfaces. So a classic example, let's say you have a back end that's written in Rust, you have some business logic that is written in rust, and you want to port it into your web application as well as your mobile applications like Android and iOS. The same binary can be shared between the web application using WebAssembly, for example, or the native model, and it can be compiled and reused inside Java or Swift, for example, so you can share the same code using pure rust. It's not true for WASM because once you compile it to a WaSM, it can only be executed by webassembly Vm. WebAssembly vms are available in all the browsers and node js, as I've said, but they are not available in mobile. For example, if you need to share your logic with your mobile application, and your mobile application is native, meaning it's written Java or Swift, then you can share the webassembly. Because as far as I know, maybe there are implementations for the Webassembly VM in Java or Swift, but as far as I know, it's not that easy. So if you're talking about reusability, the native models can be reused in other languages that support foreign function interfaces. Let's talk about ergonomics. Ergonomics is roughly the amount of code you need to write in order to produce a working example. And if you paid attention that youve saw that the amount of code we need with the neon wrapper is relatively big. You need to import all the neon types, you need to write the glue layer, you need to write the export function. And the glue layer can become very messy because you have error handling, because remember, the Javascript isn't typed, there are no types. So you need to be able to do casts between maybe it's a string, maybe it's can odd string, maybe it's a number, maybe it's not a number. So you need to handle all the edge cases. Webassembly on the other hand is typed. So webassembly have types, they have basic types, but nevertheless the waslin binding and package is able to seamlessly convert your rust types into webassembly types, which with neon requires a glue layer, as I've said. So in my opinion, the ergonomics with webassembly, especially if youve doing like small optimizations, you want to rewrite maybe two to three functions. Then the ergonomics with the webassembly compilation are a lot nicer in my opinion, especially with the bus and binding and function, because all you need to do is just macro the function and you got an executable webassembly, it's a perfectly valid rasp code. Add the macro and you get a webassembly output. Let's talk about standard library for those of you who don't know, standard library or Stdlib is talking about the access to the file system, networking and anything OS related. And it's a funny one, because when I worked on this presentation and the blog post that inspired this presentation, I learned that you actually can't access files from webassembly because Webassembly does not have access. Two, the standard library and if you look at the WaSm Biengan package, you can see that anything that is related into the OS and file system and the standard library, it's actually not implemented. Inside the code there is a proposal. It's called wazi, which stands for Webassembly system interface I things which does give you access to, which is a proposal to give you access to the native operation system, things like file system and network. But for now it's only a proposal. And if youve need to have access to the standard library, your only solution today is to use native modules. Unless you want to experiment with the webassembly system interface, which is still in development. I also found out that there is an option that you can mount like a virtual file system, so your files will be actually compiled into the webassembly itself and then you have a virtual file system like in RAM file system that you can read the files from, but it's not the same as reading dynamic files, you can't use it for if you want to read a dynamic file from the disk for example. So if that's your use case youve absolutely have to go to the native model solution. If we are talking about portability, it's the famous phrase that it works in my machine. You need to remember that native models are host machine dependent. This means that when I compile my rust code on MacBook M one it will be compiled into the ARM architecture. I can just take this binary and give it to my friend who runs a Windows machine because his windows machine is running a different architecture, the X 86 64. And this is probably most likely it's not true for WaSM because WaSM is executing by a vm. So once I have the WASM executable I can give it to the other person and as long as he have the vm he can executing the code. And for native models it's always important to recompile to the target architecture. So always remember that if you are using native models and you are running let's say on an Alpine Linux docker in your production environment, you should compile the native models on the same environment. So dockerized containers are good solutions. Don't just upload your binary files, always compile them in the required environment as you need. Let's talk about node JS and browser. And up until now I refer to NodeJs and JavaScript interchangeably. But they are two different things. JavaScript is a language, node JS is a framework, and the big takeaway that you can take from this is that native modules can't be used in the browser because JavaScript has no support for foreign function interface. The reason we can use native models inside Node Js is that NodeJs provides the so called can API, which is node API. It's an API to build native extensions with a stable API so you can write extensions in rust or in c or C and to extend your node JS ecosystem. So remember that native modules cannot be used in the browser. So your only solution for the browser is to actually use Wasm because all browsers have a VM for that except for Internet explorer as well as Node Js. But if youve don't target the browser and you target only node JS, so for example only backend code, then it's perfectly safe to write native models. Let's look at a recap. When I think. But whether to choose native models or webassembly, I like to have two mental models in my head. The first mental model says that native models are meant to extend your node JS code. So if you have a Nodejs code that you want to optimize, then you can use native modules to extend it beyond what Javascript and nodejs can provide you. While webassembly is meant to replace non performant Javascript code. So let's say you have some image processing in the browser, you want the user to be able to manipulate images. So webassembly will be a great place for this. Let's say youve developing a visualization application in the browser itself. It's very common now that many so called professional desktop tools are moving into the web, and so webassembly is a great place to squeeze all the performance that you need to squeeze from them. So those are the mental models I hold in my head. They work pretty well. And thank you very much. I hope you learned something. You can find me in LinkedIn I'm mostly active in LinkedIn. You can follow me on Twitter, you can find me in GitHub. You can scan this QR code which will lead you to my blog where you can find the articles that this talk is based on. There is more technical information inside the articles AWS, well as link to GitHub repos that you can execute the Fibonacci examples. And thank you very much and I hope you enjoyed my talk and enjoyed the rest of the conference.
...

Dmitry Kudryavtsev

Senior Software Engineer @ Forter

Dmitry Kudryavtsev's LinkedIn account Dmitry Kudryavtsev's twitter account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways