You possibly can enhance the efficiency of your Electron apps fairly a bit by offloading intensive duties to Rust.
There are 2 essential libraries on the market that will help you do that: Neon and napi-rs. Because it stands as we speak, Neon is extra in style, with over 5700 stars on Github, whereas napi-rs has solely somewhat over 800.
That stated, stars aren’t all the things! For my use case (and as of this writing) napi-rs helps an necessary function that Neon doesn’t have but: the power for Rust to name again to a JS callback perform a number of instances.
I went in quest of a minimal starter mission to get going with Electron + napi-rs, however couldn’t discover something. Therefore this publish 🙂
TL;DR: In case you simply wish to clone the mission you will discover electron-napi-rs on Github.
The remainder of this publish explains how the items match collectively.
(btw if you wish to use Neon as a substitute of napi-rs, take a look at Mike Barber’s electron-neon-rust, which is principally the Neon model of what I’m doing right here)
A Minimal Venture with Electron and napi-rs
I began with the official Electron starter from electron-quick-start. That’ll get an Electron app on the display.
Then I added the Rust module. This is kind of a copy-paste from napi-rs’s napi-derive-example, with just a few relative paths modified.
I’m placing the Rust module in a listing known as hi-rust
contained in the Electron mission. We solely want so as to add 4 information:
Cargo.toml
hi-rust/Cargo.toml
[package]
authors = ["LongYinan <lynweklm@gmail.com>"]
version = "2018"
identify = "hi-rust"
model = "0.1.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
napi = "1.7.5"
napi-derive = "1.1.0"
[build-dependencies]
napi-build = "1.1.0"
(modified to make use of model numbers as a substitute of relative paths for the [dependencies]
and [build-dependencies]
)
construct.rs
hi-rust/construct.rs
extern crate napi_build;
fn essential() {
use napi_build::setup;
setup();
}
(straight out of napi-derive-example)
This construct.rs
file is particular to Rust. You possibly can learn extra in the Construct Scripts part of the Cargo ebook, however principally Rust will search for a construct.rs
file and run it earlier than the construct, if it’s current.
src/lib.rs
Then there’s the code itself, below the src
folder:
hi-rust/src/lib.rs
#[macro_use]
extern crate napi_derive;
use napi::{CallContext, Error, JsNumber, JsObject, JsUnknown, End result, Standing};
use std::convert::TryInto;
#[module_exports]
fn init(mut exports: JsObject) -> End result<()> {
exports.create_named_method("testThrow", test_throw)?;
exports.create_named_method("fibonacci", fibonacci)?;
Okay(())
}
#[js_function]
fn test_throw(_ctx: CallContext) -> End result<JsUnknown> {
Err(Error::from_status(Standing::GenericFailure))
}
#[js_function(1)]
fn fibonacci(ctx: CallContext) -> End result<JsNumber> {
let n = ctx.get::<JsNumber>(0)?.try_into()?;
ctx.env.create_int64(fibonacci_native(n))
}
#[inline]
fn fibonacci_native(n: i64) -> i64 {
match n 2 => 1,
_ => fibonacci_native(n - 1) + fibonacci_native(n - 2),
}
(additionally straight out of the napi-rs repo)
It exposes 2 Rust capabilities to JavaScript: test_throw
and fibonacci
are uncovered as testThrow
and fibonacci
, respectively.
The init
is successfully the “entry level” for the JS <-> Rust binding, and this file might name out to no matter Rust code you need.
package deal.json
Run npm init -y
to initialize a default package deal.json, then add “construct” and “set up” scripts.
The construct script relies on a package deal for copying out the constructed Rust binary, so set up that with npm set up -D cargo-cp-artifact
.
hi-rust/package deal.json
{
"identify": "hi-rust",
"model": "1.0.0",
"description": "",
"essential": "index.node",
"scripts": {
"set up": "npm run construct",
"construct": "cargo-cp-artifact -nc index.node -- cargo construct --message-format=json-render-diagnostics"
},
"key phrases": [],
"writer": "",
"license": "ISC",
"devDependencies": {
"cargo-cp-artifact": "^0.1.4"
}
}
The construct
script successfully does 2 issues:
cargo construct
compiles the Rust module and saves the compiled file ingoal/debug
cargo-cp-artifact
copies that output into the foundation of the mission asindex.node
The set up
script simply makes it simpler to run. (You possibly can npm i
as a substitute of npm run construct
)
Launch Construct
Cargo will compile a Debug construct by default, which is slower and bigger, however incorporates debugging symbols.
Make certain to compile a Launch construct if you’d like it to be quicker and smaller! Append the --release
flag to the tip of the cargo construct
command if/while you wish to try this.
I did this straight away as a result of my app was a lot slower in Debug mode.
Apart: index.js vs index.node?
An attention-grabbing factor occurred once I was setting this up!
At first I didn’t change “essential” in any respect, and left its worth because the default index.js
. Which… labored completely effective, though there was solely an index.node file current (no index.js).
I assume Node is aware of to search for index.node
if it will probably’t discover index.js
?
Anyway, that was somewhat unnerving, so I modified the “essential” key to level on to index.node
, and that labored effective too. I determine it’s higher to level it at a file that truly exists 🤷 On the very least it’ll shave a pair cycles off the import, eh?
Construct index.node
Operating npm set up
contained in the hi-rust
listing will obtain the required packages and construct the index.node
file, which is our native Rust code, packaged up in order that Node can require()
it.
Add the Rust module as a dependency
Again within the top-level Electron mission, add the Rust module as a dependency to package deal.json:
package deal.json
{
...
"dependencies": {
"hi-rust": "./hi-rust"
}
}
Then run npm set up
and it’ll make a hyperlink to the mission.
From right here on, you may modify and rebuild the Rust mission (inside hi-rust
) with out having to re-run npm set up
.
Expose the Rust module with preload.js
We’ve got native code, it’s packaged and constructed as a module that Node can import. Now we have to import it contained in the Electron app.
There’s 2 methods to do that: the insecure means, and the higher means.
The insecure means is to set nodeIntegration: true
in order that we are able to require()
node modules instantly from our Electron renderer course of. It makes for simpler code, however the principle draw back is the large safety gap it opens up.
Why to not set nodeIntegration: true
in Electron
With the insecure setup, any JS run by the renderer has full entry to the person’s system. Meaning File APIs, Community APIs, Course of APIs, and many others, and many others.
It will probably do something the person can do. Like obtain and run some computer virus, or ransomware their house listing.
Writing the code with nodeIntegration: true
makes for barely much less problem on the expense of a gaping safety gap.
Learn extra in regards to the safety behind this within the Electron docs.
The higher means
The higher means is to make use of Electron’s preload
file to selectively expose performance to the renderer course of a.ok.a. the “essential world”, which is what we’ll do right here.
In essential.js
, the Electron starter mission units up preload.js
because the designated preload file. The preloader has entry to each Node APIs and browser APIs, however the essential distinction is that it’s remoted: the renderer can’t attain in and name stuff from preload, except preload has explicitly uncovered it.
So we expose our Rust module from preload.js
like so:
preload.js
// Import the Rust library and expose it globally as `rustLib`
// within the renderer (additionally accessible as `window.rustLib`)
const rustLib = require('hi-rust')
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('rustLib', rustLib)
Be aware this exposes the entire library! You’ll wish to pause and mirror for a second whether or not it is a good concept from a safety standpoint. If malicious code might name any of your library’s capabilities, what might occur?
As a probably safer different, you may expose particular person capabilities…
preload.js
contextBridge.exposeInMainWorld('rustLib', {
fibonacci: rustLib.fibonacci
})
Or wrap the calls in a perform, to make sure solely sure arguments are allowed by means of, or do different checks:
preload.js
contextBridge.exposeInMainWorld('rustLib', {
fibonacci: (num) => {
if (num > 42) return;
return rustLib.fibonacci(num);
}
})
You can even use Electron’s IPC system to ship requests forwards and backwards between essential and renderer processes.
Name the Rust code from Electron in renderer.js
Now we are able to lastly name the Rust perform from the renderer!
As soon as the DOM is prepared, we name rustLib.fibonacci
, referencing the uncovered international rustLib
that got here from the preload script, and retailer the end in a component (that we nonetheless have to create).
renderer.js
window.addEventListener('DOMContentLoaded', () => {
const consequence = rustLib.fibonacci(8);
const content material = doc.querySelector('#rust-content');
content material.innerHTML = `This quantity got here from Rust! <sturdy>${consequence}</sturdy>`;
});
In case you run this now you’ll most likely get an error like “Can not entry property innerHTML of null”, as a result of the aspect doesn’t exist but.
Let’s add a div with id="rust-content"
to include the consequence:
index.html
<html>
<!-- snip -->
<physique>
<!-- snip -->
<div id="rust-content"></div>
</physique>
</html>
It really works!
At this level you need to be capable of run npm begin
from the top-level (Electron) listing, and the app ought to pop up with a quantity computed by Rust 🙂
…synchronously!
One factor to notice that it is a synchronous name to Rust. If the fibonacci perform is tremendous sluggish, or we have been to name another perform that blocked, our app would freeze up.
You possibly can do that your self: strive passing a giant quantity like 1234
to fibonacci, as a substitute of 8
.
Assist! Errors!
Listed below are a pair errors I hit alongside the best way and the way I fastened them. In case you’re following alongside, you most likely received’t hit these, however I’m itemizing them right here simply in case.
A lacking package deal.json
I bought this error once I forgot to create a package deal.json
contained in the Rust library’s listing:
Inner Error: Can not discover module '/Customers/dceddia/Initiatives/electron-napi-rs/hi-rust/package deal.json' Require stack: - /usr/native/lib/node_modules/@napi-rs/cli/scripts/index.js Require stack: - /usr/native/lib/node_modules/@napi-rs/cli/scripts/index.js at Operate.Module._resolveFilename (node:inside/modules/cjs/loader:933:15) at Operate.Module._load (node:inside/modules/cjs/loader:778:27) at Module.require (node:inside/modules/cjs/loader:1005:19) at require (node:inside/modules/cjs/helpers:94:18) at getNapiConfig (/usr/native/lib/node_modules/@napi-rs/cli/scripts/index.js:23450:19) at BuildCommand.<lt;nameless> (/usr/native/lib/node_modules/@napi-rs/cli/scripts/index.js:23579:30) at Generator.subsequent (<lt;nameless>) at /usr/native/lib/node_modules/@napi-rs/cli/scripts/index.js:65:61 at new Promise (<lt;nameless>) at __async (/usr/native/lib/node_modules/@napi-rs/cli/scripts/index.js:49:10)
The repair ended up being fairly easy: npm init -y
created a package deal.json
file and solved the error.
Exporting incorrectly from Electron’s preload.js
My first try to show the Rust library to Electron’s renderer course of was one thing like:
const rustLib = require('hi-rust');
window.rustLib = rustLib;
I used to be capable of begin Electron simply effective, but it surely logged an error within the browser console, indicating that window.rustLib
was undefined… which meant my line was being ignored.
Uncaught TypeError: Can not learn property 'fibonacci' of undefined
I believe it’s as a result of contextIsolation
is ON by default, so something added to the window
object received’t be seen.
The repair was to make use of Electron’s contextBridge
module, particularly the exposeInMainWorld
perform:
preload.js
const rustLib = require('hi-rust');
const { contextBridge } = require('electron');
contextBridge.exposeInMainWorld('rustLib', rustLib)
Success! Now test your electronic mail.
Studying React generally is a battle — so many libraries and instruments!
My recommendation? Ignore all of them 🙂
For a step-by-step method, take a look at my Pure React workshop.

Be taught to suppose in React
- 90+ screencast classes
- Full transcripts and closed captions
- All of the code from the teachings
- Developer interviews
Dave Ceddia’s Pure React is a piece of monumental readability and depth. Hats off. I am a React coach in London and would totally advocate this to all entrance finish devs eager to upskill or consolidate.