Discovering Deno — WebAssembly

Geert-Jan Zwiers
Geek Culture
Published in
6 min readAug 11, 2021

--

Image credit: Pablo Barría Urenda

In recent years a new technology has emerged that can raise the bar of JavaScript application performance: WebAssembly. In this post we are going to to make a WebAssembly module to generate Hello World greetings and serve them using Deno. To achieve this we will need to make a small Rust crate (library), compile it to a binary using the wasm-bindgen and wasm-pack tools, optimize it for both speed and size and finally load it into a Deno HTTP server app in order to send it across the interwebs.

So what do all these terms stand for? WebAssembly is an assembly language designed for use on the web (surprise!). Rust is a low-level systems programming language that can compile to WebAssembly and Deno is a JavaScript/TypeScript runtime which is built in Rust and supports the use of WebAssembly. Basically, Rust + WebAssembly runs much faster than JavaScript (similar to C/C++ in speed) and was designed to be used alongside it to improve the performance of key application components.

Let’s get started!

The Rust Part

Rust is not actually named after the oxidation of iron but after the Rust Spotted Guard Crab, which explains why Rust’s official logo is a tiny crustacean. If it were named after the former that would be a bit ironic, to say the least, because Rust is not exactly rusty!

To get started we have to install Rust and its package manager Cargo. You can find them both here. Once you are set up, use Cargo to install wasm-pack which creates the packaging needed to run the compiled WebAssembly:

cargo install wasm-pack

Next, create a new package in your directory of choice:

cargo new --lib hello-wasm

The new project contains aCargo.toml file which, in case you’re familiar with Node.js/npm, is a bit like package.json We need to configure the Rust project so that its code can be understood and used from JavaScript. To do this, add the following to Cargo.toml:

[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"

In the snippet above, crate-type = ["cdylib"] tells Rust to produce a library to be loaded from another language. wasm_bindgen is a library that will do the hard work of creating bindings between Rust and JavaScript types.

Now let’s go to the file src/lib.rs which contains some test code. Replace it with the following lines:

use wasm_bindgen::prelude::wasm_bindgen;#[wasm_bindgen]
pub fn greet(name: &str) -> String {
return format!("Hello, {}!", name);
}

Here we import wasm_bindgen and place a #[wasm_bindgen] annotation on top of a function. Now if this greet function is called from a JavaScript program it will know how to map a JavaScript string to a Rust string and vice-versa.

The Assembly Part a.k.a Greeting Library Optimization

It is time to compile the Rust code to WebAssembly binaries, but before doing so we want to perform some fine-tuning. One great thing about Rust crates is that they can be optimized for speed and size in various ways. Today’s big thing in software engineering is, of course, not blockchain nor machine learning, but Greeting Library Optimization. In a fast-paced digital world users must be greeted as fast as possible or they will surf away, never to return. :)

We can add some configuration to Cargo.toml to optimize the crate with the following section:

[profile.release]
lto = true
opt-level = "s"

Here we enable Link Time Optimization (LTO) which decreases the binary size and makes the crate faster, at the cost of some increased compilation time. We also set opt-level to s to optimize for speed. This can also be set to z to optimize for size, but interestingly opt-level = "s" can lead to smaller binaries and it is really a matter of testing to see which one works best. Let’s compile the Rust crate to WebAssembly with wasm-pack:

wasm-pack build --target web

--target web will generate binaries that work with Deno, amongst other web targets like browsers. You should now have a pkg directory in the project with a hello_wasm_bg.wasm file and some .js and .d.ts files as well.

More optimization can be done using a build tool called wasm-opt, which is part of the larger Binaryen library. NPM is an easy way to install it for a small project like this, otherwise you can find the tools on their GitHub Releases page also.

npm install -g binaryen

Now you should be able to call wasm-opt from a terminal and perform another optimization. Note that you need to point it to the .wasm file in the pkg directory that was created when you called wasm-pack build:

wasm-opt -O -o output.wasm pkg/hello_wasm_bg.wasm

If you are on a Linux machine or use Windows with WSL installed you can use the wc tool with -c to get the byte count of the binary:

wc -c output.wasm

This gave the following results after running it for each optimization:

Before optimizations: 16276 bytes
With lto = true: 14083 bytes
With lto + opt-level s: 13894 bytes
With lto + opt-level + wasm-opt -o -O: 13812 bytes

The byte size has been reduced with 2914 bytes! Note that it would be possible to slim down the binary even more with compression tools such as gzip or brotli, but since we are not sending this WebAssembly over a network for this post it will not be necessary. Keep in mind, however, that many optimizations involve a trade-off between execution speed and binary size. Now, it is about time we bring this code to Deno land.

The Deno Part

Image credit: Hashrock

Make a file server.ts in the project, preferably outside of the src directory with the Rust code and the pkg directory with the WebAssembly. Start by importing the init and greet functions that were generated with wasm-pack:

import init, { greet } from "./pkg/hello_wasm.js";

Use the init function to initialize the WebAssembly module, which we can load from the filesystem with Deno.readFile:

await init(Deno.readFile("./pkg/hello_wasm_bg.wasm"));

Now we are ready to call greet and send the greeting to a client. If you look at the greet function in pkg/hello_wasm.js you can see it calls to the WebAssembly binary to make the greeting. To serve it up to the browser we need a simple HTTP server. For this example we can use Deno’s native server, though you could also pick any of the plentiful third-party HTTP server modules in Deno land or the one from the Deno Standard Library std/http. The code for the native server is shown below:

import init, { greet } from "./pkg/hello_wasm.js";await init(Deno.readFile("./pkg/hello_wasm_bg.wasm"));const server = Deno.listen({ port: 8080 });for await (const conn of server) {
handle(conn);
}
async function handle(conn: Deno.Conn) {
const httpConn = Deno.serveHttp(conn);

for await (const requestEvent of httpConn) {
try {
await requestEvent.respondWith(new Response(
greet("World"), {
status: 200,
headers: {
"content-type": "text/html",
},
},
),
);
} catch (e) {
console.error(e);
}
}
}

Note that we are sending back a Response object with the results of the greet function. If you run this server with deno run --allow-net --allow-read server.ts you should be able to go to localhost in a browser and receive a Hello World! The result may seem trivial, but this greeting has been generated with the much faster WebAssembly compiler instead of JavaScript’s Just-In-Time compiler used by browsers. For real world applications this can have a major impact on performance and it is being used in more and more places to speed up the web.

This post was inspired by this guide on MDN and this post on The New Stack amongst others. I hope you enjoyed it and learned a thing or two! Till next time!

--

--