WebAssembly (Wasm) is a binary instruction format (similar to assembly) supported by all major browsers and various runtimes. It is nearly as fast as natively compiled code while being cross-platform and sandboxed. WebAssembly, like assembly, is normally used as a compilation target for compiled languages like Rust, C, C++, C# and others (some interpreted languages can also run in Wasm). JavaScript can interface with WebAssembly to leverage its power, but this should be done with care as switching from JavaScript to WebAssembly has overhead (upfront load times and possible serialization / deserialization across the boundary), which can nullify the advantage of using WebAssembly if the switch is made too often.
- Install Rust
- Install Node.js
- For Deno /
cargo-make
- Install
cargo-make
- The included
Make.toml
will automatically installdeno
,wasm-bindgen
, and add thewasm32-unknown-unknown
target torustup
as needed.
- The included
- Install
- For Node.js using
wasm-pack
We’ve prepared a repository that allows you to easily get started with compiling Rust to Wasm, currently focusing on the following targets (JavaScript / TypeScript):
The repository supports the following 3 methods of instrumentation for building and running Rust (compiled to WebAssembly) + .js
/ .ts
files:
cargo-make
(Node.js, Deno)
Cargo make allows you to define and run tasks in .toml
files, while specifying various dependencies and allowing for a lot of useful instrumentation around your tasks. Here we use cargo-make
to invoke wasm-bindgen-cli
(which is partially what wasm-pack
does, but it does not support Deno for example), deno
, node
and others. This condenses the entire build into cargo make build/run-target
.
wasm-pack
(Node.js)
wasm-pack
is a tool seeking to simplify workflows involving compiling Rust to WebAssembly for use in the Browser or Node.js. For Node.js this is the simplest flow, but wasm-pack
does not currently support Deno as a target. As the Webpack flow will already invoke wasm-pack
we don’t recommend using this directly if you’re using Webpack.
- Webpack (Browser)
Webpack’s Rust compilation is mostly equivalent to option 2, with a different target flag (bundler
) for wasm-pack
and added bundling / Webpack functionality for JS. We’ll go over the configuration for Webpack later in this guide.
To compile Rust to Wasm, we first start with our Rust entry point. This is a normal lib.rs
file (as we intent to export functions from Wasm). In our example, our file looks like this:
use wasm_bindgen::prelude::*; // ¹
#[wasm_bindgen] // ²
pub fn greeter(name: &str) -> Result<String, JsError /* ³ */> {
Ok(format!("Hello {name}!"))
}
#[wasm_bindgen(start)] // ⁴
fn main() {
console_error_panic_hook::set_once(); // ⁵
}
This file, which has a greeter
function that outputs Ok(format!("Hello {name}!"))
(more on the error value later), and a main
function. This mostly looks like normal Rust, other than a few key points:
¹ use wasm_bindgen::prelude::*
- wasm_bindgen
is a library and CLI that exposes high-level interactions between Rust and JavaScript. It generates glue code and bindings for your Rust code and automates things that would otherwise need to be done manually. Here we’re use
ing wasm_bindgen
's prelude, which exports a few convenience types to be used when interfacing with JS. See other points for our usage in this example.
² #[wasm_bindgen]
- This is an example of using the wasm_bindgen
attribute macro, which exposes a function to JavaScript. This allows us to call our Rust code from JavaScript by name. The function using the attribute must be public.
³ JsError
- This type encodes a JavaScript error, which allows Rust to emulate the behavior of a normal JavaScript function. If we so desired we could also return a non Result
value.
⁴ Here we’re using the wasm_bindgen
macro with the argument start
, which exposes a function that runs only once when the Wasm binary is loaded. We can use this section of the code to prepare various things or set hooks.
⁵ console_error_panic_hook::set_once()
- console_error_panic_hook
prints Rust panics (be it from panic!()
, todo!()
, unreachable!()
or any other operation that can panic) to the console (via JavaScript console.error()
) to make panics user / debug visible and emulate JavaScript behavior. Using set_once
here ensures that additional invocations do not set the hook again.
As this guide deals with Webpack, Node.js and Deno, our example does not contain use of web_sys
, a crate that exposes browser APIs (e.g. window
, document
) to Rust. web_sys
is split into many granular features so that you can use only what you need. Similarly, if you wish to interface with JavaScript builtins (e.g. JSON
, Math
) you can use js_sys
. See also wasm-bindgen-futures
- Converts between JavaScript Promise
s to Rust Future
s.
If you just need to expose pure Rust functionality, you don’t need to use the _sys
crates. Note that interfacing back and forth between Rust and JavaScript has a performance overhead as stated above, switches back and forth should be minimized in performance sensitive contexts.
Our Cargo.toml
(Rust manifest file) is as follows:
[package]
name = "getting_started_with_rust_wasm"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
homepage = "https://grafbase.com/blog"
repository = "https://github.com/grafbase/getting-started-with-rust-wasm"
# ¹
[lib]
crate-type = ["cdylib"]
[dependencies]
# ²
wasm-bindgen = "0.2"
# ³
console_error_panic_hook = "0.1"
# ⁴
[profile.release]
opt-level = "z"
strip = true
lto = true
codegen-units = 1
¹ crate-type = ["cdylib"]
- This specifies that our will be built as a C compatible dynamic library, which helps cargo
pass the correct flags to the Rust compiler when targeting wasm32
.
² wasm-bindgen
- See lib.rs
above
³ console_error_panic_hook
- See lib.rs
above
⁴ [profile.release]
- As we’re targeting Wasm which may be transmitted over the wire / read at runtime by a browser, these are a few optimizations meant to prioritize binary size over compilation speed (except in the case of opt-level
which also affects execution speed).
opt-level
- By setting theopt-level
to"z"
, we’re instructing the Rust compiler to reduce LLVM’s inline threshold, which effectively means less code at the price of more function calls. Normally LLVM can decide to inline a function in more cases which would result in a larger but more performant binary. We could also use"s"
here to only partially make this change (allow loop vectorization).strip = true
- This strips everything (symbols, debuginfo) from the binary. This reduces the resulting binary size at the cost of losing the names of functions in backtraces and other debug info.lto = true
- This instructs LLVM to perform link time optimization (LTO), which is not used by default. LTO is slower when compiling but can perform additional optimizations which can reduce the resulting binary size.codegen-units = 1
- This setting normally allows splitting a crate into multiple “codegen units”, allowing LLVM to process the crate faster but giving up on certain optimizations which may reduce size. Setting this to1
enables LLVM to perform those optimizations at the cost of slower compile times.- See johnthagen/min-sized-rust for more details
Our Node.js and Deno files are similar, and are as follows:
import { greeter } from '../pkg/getting_started_with_rust_wasm.js'
const greeting = greeter('Grafbase')
console.log({ greeting })
Both are just a simple import and usage of the function we exposed from WebAssembly, with a log of the resulting value for demonstration. Our build methods do all of the work here so no extra configuration is needed.
Our Webpack entrypoint is similar to the above:
import { greeter } from '../pkg'
let greeting = greeter('Grafbase')
document.getElementById('root').innerText = greeting
With the distinction that it imports the entire pkg
directory, and writes the result of greeter
to the DOM rather than to the console.
This target requires an additional configuration for Webpack, seen here:
import WasmPackPlugin from '@wasm-tool/wasm-pack-plugin'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import path from 'path'
import { fileURLToPath } from 'url'
// these are needed since we're using a .mjs file
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
export default {
entry: './src/bundler.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/bundler.html',
}),
// ¹
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, '.'),
}),
],
// ²
experiments: { asyncWebAssembly: true },
mode: 'production',
}
¹ WasmPackPlugin
- This plugin allows Webpack to run wasm-pack
directly and reload when Rust files change
² experiments: { asyncWebAssembly: true }
- Supports the new WebAssembly specification which makes WebAssembly modules async. Required for working with the output of wasm-pack
.
The different methods of compilation detailed above will all result in a pkg
directory in the root of the repo with a compiled .wasm
file or files, glue JS code, TypeScript type definitions and possibly various files for deployment (e.g. package.json
, README.md
) depending on the method you used. This output is then imported by the various .ts
/ .js
files and used.
These are the artifact sizes for the provided example repository build:
- WebAssembly:
~22.5 KB
(21-24 KB depending on the target) - JavaScript:
5 KB
Note that we do not remove the standard library or perform a few other case dependent optimizations in the provided repository, see johnthagen/min-sized-rust for possible additional optimizations that may fit your use-case. WebAssembly binaries may also be lazy-loaded when needed / after page initialization to improve performance if needed.
- With
wasm-pack
wasm-pack build --target nodejs
- Builds and generates bindings forsrc/lib.rs
node src/node.mjs
- Runssrc/node.mjs
- With
cargo-make
cargo make run-node
- Runssrc/node.mjs
npm run serve
- Open http://localhost:8080
cargo make run-deno
- Runssrc/deno.ts
In this guide we went over what WebAssembly is, how to compile Rust to WebAssembly for use in different targets and best practices for doing so. Make sure to check out the linked repository and CodeSandbox!
- We've added a Nix flake as an additional setup option for the linked repository. For more details see README.md#with-nix
Grafbase - Instant GraphQL APIs for Your Data
Grafbase enables developers to ship data APIs faster with modern tooling.
At Grafbase we use WebAssembly to deploy your API to the edge: preventing cold-starts, caching your API close to your users and allowing you to write business logic in any language you choose that compiles to WebAssembly (coming soon).
You can find our public GitHub repository here.