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.
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()
andfactorialize_fib()
in both Javascript and Rust in his example source code to understand how is WebAssembly faster forfactorialize_fib()
, but slower than JS on the simplerfactorize()
.
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:
- Use a provided wasm module via npm
- Hook your own local wasm npm module into an app
- 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 :)
Top comments (0)