WebAssembly is one of the newest technologies to hit the web dev world with some promising new features around performance. This is a look into how we could slowly integrate the new technology into an existing React app.
WebAssembly is one of the newest technologies in web development. It allows you to execute code built in other languages — a feature you can take advantage of without a major rewrite, since we can incorporate it with existing code bases. Since the easiest way to gain adoption of new technology is to slowly weave it into an existing code base, we are going to be taking a React app that is built with create-react-app and adding WebAssembly libraries that were built in Rust. It's pretty common to have more than one team working on a React app (frontend + backend), and I can't think of a cooler experience than sharing code without sharing a language.
The source code for this article can be found on GitHub: react-wasm-migration and react-wasm-rust-library.
I started with creating a React app using the boilerplate.
npx create-react-app react-wasm-migration
Out of the box, create-react-app will not support WebAssembly. We have to make some changes to the underlying webpack config that powers the app. Unfortunately, create-react-app doesn't expose the webpack config file. So, we'll need to pull in some dev dependencies to help out. react-app-rewired
is going to allow us to modify the webpack without ejecting, and wasm-load
will help webpack handle WebAssembly.
Yarn:
yarn add react-app-rewired wasm-loader --dev
npm:
npm install react-app-rewired wasm-loader -D
Once this is done, you should have a fully functioning app, and we can jump into making some tweaks to our webpack.
We need to add a config-overrides.js
file to the root of our app. This file will allow us to make changes to our webpack file without rewriting it.
const path = require('path');
module.exports = function override(config, env) {
const wasmExtensionRegExp = /\.wasm$/;
config.resolve.extensions.push('.wasm');
config.module.rules.forEach(rule => {
(rule.oneOf || []).forEach(oneOf => {
if (oneOf.loader && oneOf.loader.indexOf('file-loader') >= 0) {
// make file-loader ignore WASM files
oneOf.exclude.push(wasmExtensionRegExp);
}
});
});
// add a dedicated loader for WASM
config.module.rules.push({
test: wasmExtensionRegExp,
include: path.resolve(__dirname, 'src'),
use: [{ loader: require.resolve('wasm-loader'), options: {} }]
});
return config;
};
Credit for the above file goes to the folks over in Wasm Loader GitHub Issues, who were working towards the same goal of getting WebAssembly into a create-react-app.
At this point, if you run yarn start
, you will not be using the webpack config changes, since we need to modify the package scripts. We need to make some changes to package.json
in order to take advantage of the changes we just made.
Old:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test"
}
New:
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
}
If you run yarn start
, you should see the same initial page for a create-react-app. After each step, you should have a working application.
There are several guides on creating WebAssembly in your language of choice, so we are going to gloss over such creation in this post. I've attached a link to the repo that I used to create the .wasm file that we are going to be using for this application. You can check it out along with some details on how I created it at react-wasm-rust-library.
At this point, our React app can support WebAssembly — we just need to include it within the app. I've copied my WebAssembly package into a new folder called “external” at the root level.
For the WebAssembly, we have added hello
, add
, and sub
functions. Hello
takes a string and returns Hello, <string>
. Add
will take two numbers and return their sum. Sub
will take two numbers and return their difference.
Next up, we need to add our Wasm to our package.json
and install it using yarn install --force
or npm install
.
dependencies: {
"external": "file:./external"
}
This is not standard — we are actually skipping the step where we publish the WebAssembly package to npm and install it like any other node dependency. For production, you would want to publish your WebAssembly package to a private or public npm and install it using Yarn or npm.
We have everything in place to support WebAssembly; Webpack has been modified to support WebAssembly and we have included our WebAssembly package into our app. The last step is to start using the code.
WebAssembly must be loaded asynchronous, so we must include it using the import statement in App.js
:
componentDidMount() {
this.loadWasm();
}
loadWasm = async () => {
try {
const wasm = await import('external');
this.setState({wasm});
} catch(err) {
console.error(`Unexpected error in loadWasm. [Message: ${err.message}]`);
}
};
This will give us access to the WebAssembly as this.state.wasm
. Next, we need to utilize our library.
render() {
const { wasm = {} } = this.state;
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Edit <code>src/App.js</code> and save to reload.</p>
<a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a>
<div>
<div>Name: <input type='text' onChange={(e) => this.setState({name: e.target.value})} /></div>
<div>{ wasm.hello && wasm.hello(this.state.name) } </div>
</div>
<div>
<div>
Add:
<input type='text' onChange={(e) => this.setState({addNum1: e.target.value})} />
<input type='text' onChange={(e) => this.setState({addNum2: e.target.value})} />
</div>
<div>
Result:
{ wasm.add && wasm.add(this.state.addNum1 || 0, this.state.addNum2 || 0) }
</div>
</div>
<div>
<div>
Sub:
<input type='text' onChange={(e) => this.setState({subNum1: e.target.value})} />
<input type='text' onChange={(e) => this.setState({subNum2: e.target.value})} />
</div>
<div>
Result:
{ wasm.sub && wasm.sub(this.state.subNum1 || 0, this.state.subNum2 || 0) }
</div>
</div>
</header>
</div>
);
At this point, you can yarn start
and start interacting with your WebAssembly.
You can see how this can be pretty game-changing in places where you have teams working in different languages but need to collaborate on common deliverables, since you can share code instead of contracts. There are definitely some anti-patterns to watch out for as you begin your WebAssembly journey, though.
You will want to keep your libraries small since they cannot be bundled with the rest of your code. If you find that you are creating a massive WebAssembly, it may be time to break it up into smaller pieces.
You shouldn't WebAssembly-ify everything. If you know that the code is frontend only and there is no reason to share it, it may be easier to write it in JavaScript and maintain until you can verify that WebAssembly will make it perform faster.
Hopefully you feel that adding WebAssembly into your React project is well within reach after reading over this article.
Richard Reedy has been working in the software field for over 12 years. He has worked on everything from operations to backend server development to really awesome frontend UI. He enjoys building great products and the teams around them. His latest venture is enabling technology to better serve food trucks around the United States.