loading...

Embed Rust wasm into React

techtrouts profile image Carlos Ouro ・6 min read

WebAssembly is coming out to play. It's time for us, developers, to also move forward and push our heavy lifting computation algorithms into the low-level architecture in our products/libs/components and, together, speed up the web client as a whole.

What should know (at a high-level) before we progress:

  • Modern JavaScript;
  • React;
  • react-app-rewired pipeline;
  • Rust;

If you are looking for a non-Rust WASM approach

First, let's keep in mind how WebAssembly actually runs in a web page.

How does WebAssembly run in a webpage?

WebAssembly is a low-level module with a sandboxed shared memory allocated and managed by the browser.
Today, we instance these WebAssembly modules via JavaScript and can then interop with their shared memory and call exported module functionality.

WebAssembly runtime

Now we're ready to see how we can make our Rust code take some algorithmic load from our modern React web app.


When to use WASM instead of JS

JavaScript does an epic job in the web platform - it's perfect to describe events, functional flows and passing arbitrary small sets of data around. It has direct runtime scope integration into the window, giving you direct scope interoperability between different JavaScript modules loaded on the page with a simple single-threaded approach.
Perhaps one day we'll find an even better approach, but that is not the goal of WebAssembly.

WebAssembly thrives in heavy data manipulation and complex algorithmic logic and, last but not least, large chains of function calls.
Think about image/video/sound data manipulation, heavy string operations, game/scene object interpolations, etc.

You can read more at this great article by Koala42, where, near the bottom, he demonstrates how can WebAssembly be much slower or much faster than JS in 2 fairly similar tasks.

If you are curious, check the difference of factorize() and factorialize_fib() in both Javascript and Rust in his example source code to understand how is WebAssembly faster for factorialize_fib(), but slower than JS on the simpler factorize().


But the stack milord!

Indeed the modern web app is not bare metal anymore. Our JSX+ES7 codebase and 3rd party modules are bundled on-the-fly by complex build pipelines transpiling, collating and cleaning up code into a shinny optimised web app output that we actually don't know much about anymore.
This process takes away most of the cross-browser/transpile pain, but makes it hard to introduce something new or tweak under the hood.

Rant: I hope web standards adoption trends and perhaps a standardised react alternative (Web Components?) will one day bring us back to simpler build pipelines, closer to bare metal web and JS...

So, how do we integrate the modern stack with WebAssembly?

Back to the future

Let's assume you have some kind of react app with a typical create-react-app base template structure. This example purposely showcases the git repo.

- myApp
  | - .git/
  | - node_modules/
  | - public/
  | - src/
  | - config-overrides.js
  | - package.json



There are 3 different approaches to integrating WASM modules into your react app:

  1. Use a provided wasm module via npm
  2. Hook your own local wasm npm module into an app
  3. Embed a wasm module directly into your app git repo



Use case 1. Use a provided wasm module via npm

This option is so simple you may even already be using 3rd party WASM modules without knowing.

You just have to add the published npm module into your package.json and use it directly.

npm install rust-wasm-react-calculator --save

Then simply use it in your react source code

// import
import { calculate } from "rust-wasm-react-calculator";

// and use
alert(calculate("14+5"));

I know, that's way too simple - this is why we should be using WebAssembly today for whatever is meaningful in terms of performance (keep in mind - it's actually slower for most common tasks!)

Next, let's see how we can create our own


Use case 2. Hook your own local wasm npm module into an app

First, in order to create and manage your wasm npm module, let's make sure you have wasm-pack installed with all permissions it needs

sudo npm i -g wasm-pack --unsafe-perm=true

Then, outside our app codebase, let's create our hello world wasm npm module

wasm-pack new helloWorld

You will get something like

- myApp
  | - .git/
  | - node_modules/
  | - public/
  | - src/
  | - config-overrides.js
  | - package.json
- helloWorld
  | - .git/
  | - src/
  | - tests/
  | - ... cargo files, etc

You now can see the actual Rust source code generated in helloWorld/src/lib.rs.
The public methods here will be available to be called in JS, and #wasm-bindgen takes care of passing things around on our behalf.
Read more about wasm-bindgen if you need to know how it works in greater depth.

Our piece of interesting code in helloWorld/src/lib.rs:

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, worldview test!");
}

With a quick wasm-pack build an actual npm module will be generated into helloWorld/pkg - this is a ready-made wasm npm module with all the methods and bindgen stuff in it - much like the rust-wasm-react-calculator one we used for the example (1.)

To test it locally with your app, you can import it directly as a local npm module in your package.json with

{
  //...
  dependencies: {
    //...
    "helloWorldWasm": "file:../helloWorld/pkg"
  }
}

and use it in your app code like

// import
import { greet } from "helloWorldWasm";

// and use
greet();

Note: auto-update of the development server will not work out of the box - you will need to first rebuild helloWorld and then refresh your app.



Use case 3. Embed a wasm module directly into your app git repo

Finally, we come to the option where you really want to make rust part of your app and its source code.

We start off similarly to 2., by creating our own wasm-pack module.

Like before, in order to create and manage your wasm npm module, let's make sure you have wasm-pack installed with all permissions it needs

sudo npm i -g wasm-pack --unsafe-perm=true

Then, in the root of your app source, let's create our hello world wasm npm module

wasm-pack new helloHelper

You will get something like

- myApp
  | - .git/
  | - node_modules/
  | - public/
  | - src/
  | - config-overrides.js
  | - package.json
  | - helloHelper
     | - .git/
     | - src/
     | - tests/
     | - ... cargo files, etc

Next we need to remove .git from helloHelper. We do not want to have a submodule here, we want to have our helper as part of our main app repo itself.

rm -rf helloHelper/.git/

Note: It would be much easier if wasm-pack would allow us to create a module directly without VCS embedded, I've pointed this out as a feature request in wasm-pack

The final step is to hook it up to our react build, for this we will leverage wasm-pack-plugin.
Kick off by adding it to your app

npm i @wasm-tool/wasm-pack-plugin --save

Now we will let wasm-pack-plugin manage the wasm-pack build on our behalf on its own, with its own watcher and outputting the npm module (pkg) of helloHelper directly into our own app /src code. From there the react watcher itself also picks it up automatically and refreshes our app automatically when running locally.

To achieve this we need to hook helloHelper into our config-overrides.js using @wasm-tool/wasm-pack-plugin and make it part of the build. Additionally, we also need to ensure file-loader does not attempt to load .wasm file on its own.

In my case I am already using customize-cra, so I will just add two config filter methods of my own, but you could modify config directly in the same way.

const path = require("path");
const {
  override
} = require("customize-cra");

const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

module.exports = override(
  // make the file loader ignore wasm files
  config => {
    config.module.rules.find(rule => {
      return (rule.oneOf || []).find(item => {
        if (item.loader && item.loader.indexOf("file-loader") >= 0) {
          item.exclude.push(/\.wasm$/); //exclude wasm
          return true; //ignore remaining rules
        }
      });
    });

    return config;
  },

  //hook up our helloHelper wasm module
  config => {
    config.plugins = (config.plugins || []).concat([
      new WasmPackPlugin({
        crateDirectory: path.resolve(__dirname, "./helloHelper"),
        extraArgs: "--no-typescript",
        outDir: path.resolve(__dirname, "./src/helloHelperWasm")
      })
    ]);

    return config;
  }
);

And then we simply use our new local module directly in react:

// import
import { greet } from "./helloHelperWasm";

// and use
greet();

There you have it - let's npm start and let the real fun begin :)

Discussion

pic
Editor guide