Discovering Deno — WebAssembly
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
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!