First I create a simple WebAssembly "Hello World" application using Rust, explaining how to set up your development environment, write some Rust code, and then compile it to WebAssembly.
Then I follow up with a small function that uses Wasm to resize an image.
See Source Code: https://github.com/davesag/image_resize_and_combine where I've continued to experiment after finishing this article.
See also: https://davesag.medium.com/rust-from-a-typescript-developers-perspective-57ab633c4519
Background: As a JavaScript / TypeScript dev, why learn Rust or WebAssembly?
WebAssembly is a browser based low-level operating system that lets you write code in a handful of languages and compile it into a low-level universal browser-based virtual machine's code that runs as close to bare-metal as the browser is capable. You can leverage multi-processor threading, do background tasks, and you get to code in Ruby which is fast becoming my new joy.
I've been a JavaScript developer since the 1990s and regularly railed against TypeScript until suddenly I didn't. I drank the Kool Aid and suddenly found myself firing up a TS environment any time I wanted to throw together even a small experiment. I wanted to use types, and appreciated the feedback VS Code was giving me. But, in hindsight, TypeScript was simply a gateway language. The shift in mindset, driven in no small part by VS Code, gave me the impetus to keep going and look for even stronger typing, but without all the OO baggage.
Lots of work people use and adore Go, which is certainly strongly typed, but it's also very verbose. Coming from TypeScript it may as well be Objective C. So I started looking at Rust, and wow did I like what I saw.
And simply turning on the VS Code standard Rust extension is spookily good. VS Code predicts and adds type info to your code that's not actually there in the text. It's freaky, but I digress.
TL;DR: Learn Rust, it's really nice, especially coming from TypeScript. Use small WebAssembly functions to offload complex needs to a lower-level stack. Ideal applications might include image manipulation or analysis, real-time multiplayer gaming, or doing complex mathematics.
Let's write a hello world WebAssembly function!
Step 1: Set Up Your Environment
- Install Rust: Make sure you have Rust installed. You can install it from the official website.
- Add the
wasm32WebAssembly target: run the following command:
rustup target add wasm32-unknown-unknown3. Install wasm-pack: a tool for building and working with Rust-generated WebAssembly. You can install it by running:
cargo install wasm-packStep 2: Create a New Project
Create a new Rust project by running the following command:
cargo new wasm_hello_world
cd wasm_hello_worldStep 3: Write some Rust Code
Edit Cargo.toml: add the following lines to specify the crate type as cdylib and add a dependency on the wasm-bindgen crate:
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"Create a file named src/lib.rs and write the following Rust code to create a function that returns a "Hello, World!" message:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet() -> String {
"Hello, World!".to_string()
}Step 4: Compile to WebAssembly
wasm-pack build --target webSee the docs.
Step 5: Create a web page that calls the WebAssembly code
Create an HTML file named index.html in the project's root directory with the following content to call the WebAssembly module:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World in WebAssembly</title>
</head>
<body>
<p id="output"></p>
<script type="module">
import init, { greet } from './pkg/wasm_hello_world.js';
async function run() {
const greeting = await greet()
document.getElementById('output').innerText = greeting;
}
init().then(run);
</script>
</body>
</html>Step 6: Run it
You can use a simple HTTP server to serve your application. One such server is simply called http-server.
npx http-server .Now open a web browser and access the application by visiting http://localhost:8080.
You will see "Hello, World!".
Your Rust function was successfully called from WebAssembly. Hooray!

Let's do something more complicated. Image resizing.
Update index.html as follows to import a resize_image function from our Wasm code. (The image is one of mine off Flickr)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image resize in WebAssembly</title>
</head>
<body>
<p id="output"></p>
<script type="module">
import init, { greet, resize_image } from './pkg/wasm_hello_world.js';
async function run() {
const greeting = await greet()
document.getElementById('output').innerText = greeting;
const imageData = await fetch('https://live.staticflickr.com/65535/52152800977_a7ea308db0_b_d.jpg')
.then(r => r.arrayBuffer())
.then(arr => resize_image(new Uint8Array(arr), 100))
const blob = new Blob([imageData], { type: 'image/png' })
const src = URL.createObjectURL(blob)
const img = document.createElement('img')
img.src = src
document.body.appendChild(img)
}
init().then(run);
</script>
</body>
</html>This will use fetch to fetch an image then convert it to an arrayBuffer. This is passed to our resize_image function as a Uint8Array and told to resize it to a100px wide square.
The data is returned as an array of bytes so we need to convert that to a Blob (which we'll call blob). Now we can convert blob to a URL the browser understands, create an img tag set the src and attach it to the document.
Let's write more Rust!
Add image as a dependency to the cargo.toml file (check crates.io for the latest versions — weirdly there doesn't seem to be an equivalent to npm outdated in the Cargo world.)
[dependencies]
wasm-bindgen = "0.2"
image = "0.23.14"Rebuild via cargo to get image to install:
cargo buildAdd a resize_image function to the src/lib.rs file.
use wasm_bindgen::prelude::*;
use image::codecs::png::PngEncoder;
use image::imageops::resize;
use image::imageops::FilterType;
use image::ColorType;
use wasm_bindgen;
#[wasm_bindgen]
pub fn greet() -> String {
"Hello, World!".to_string()
}
#[wasm_bindgen]
pub fn resize_image(image_data: &[u8], width: u32) -> Vec<u8> {
let img = image::load_from_memory(image_data).unwrap();
let new_img = resize(&img, width, width, FilterType::Lanczos3);
let width = new_img.width();
let height = new_img.height();
let mut buf = Vec::new();
PngEncoder::new(&mut buf)
.encode(&new_img.into_raw(), width, height, ColorType::Rgba8)
.unwrap();
buf
}The resize_image function takes in image_data as a byte array and a width and returns a Vector of bytes¹. We load the image from the raw image_data (unwrapping² to say we don't care about errors here), then we resize it using a standard Lanczos3 scaling filter. Finally we use a PngEncoder to write the resized image data back into a Vector as an Rgba8 image.
Rebuild it all, re-run the web server, and go to localhost:8080 — tadah!
wasm-pack build --target web
npx http-server .
Links
- https://github.com/davesag/image_resize_and_combine
- https://davesag.medium.com/rust-from-a-typescript-developers-perspective-57ab633c4519
- https://www.rust-lang.org/tools/install
- https://rustwasm.github.io/docs/wasm-pack/commands/build.html
- https://github.com/http-party/http-server
- https://en.wikipedia.org/wiki/Lanczos_resampling
- https://crates.io
- https://mage.space
- https://doc.rust-lang.org/std/macro.panic.html
Footnotes
¹ Similarly to Java³, in Rust Arrays are fixed length whereas Vectors can change their length, and insert and remove elements.
² The unwrap function simply takes the Result type returned by the function and, if the Result is Ok, it passes Ok's value back, otherwise it shits itself, or panics, as Rust devs say. It's shorthand for "I know this could throw an error and I really don't care."
³ At least when I last wrote Java anyway, which was around 2005.
—
Like this but not a subscriber? You can support the author by joining via davesag.medium.com.