🤔 Why use WASM Modules with Deno?
Deno Fresh WASM is pretty easy to set up, just by adding a single dependency to your project. This lets you write code in Rust, compile it to WASM and then use that generated module in your Deno project. That is all straightforward, but why would you do it? We’ll look at three reasons here quickly before cracking on and seeing how to set it all up.
For me the main reason was to help learn Rust. Oftentimes you just need to write small chunks of code for your module so, shoehorning a little Rust WASM into a side-project delivers the benefits of practising Rust. That is without a heavy payout in terms of time investment. More practical reasons for using WASM could be to optimise code and also to leverage libraries in the Rust ecosystem. Image processing is a great example for optimisation here. Although there is the amazing sharp image plugin (based on the C libvips image processing library), links to Node mean it will not run in all environments. In fact we look at image processing in this post. We will do some basic image manipulation using the Rust photon-rs library.
🧱 What are we Building?
As just mentioned we will be using the photon-rs library within Rust. The main goal is to see an example of integrating Rust WASM into Deno, so we will keep the Rust side simple. In fact, the main Rust function will only have 18 lines of code. This means you can follow along even with minimal Rust knowledge.
The Rust WASM module will generate a Base64 low resolution image placeholder from an image file saved within our project. On a slow connection, it might take a couple of seconds for the full-sized image to load. We show that Base64 image placeholder in the interim. This is a nice pattern to use on websites as it allows you to reduce Cumulative Layout Shift as well as make the page look more interactive while still loading. Overall this contributes to a better user experience.
You can follow along starting from scratch or just read and get the code for the completed project in the Rodney Lab GitHub repo (link further down).
🍋 Spinning up a Fresh Deno Project
To get going let’s spin up a fresh Deno project:
deno run -A -r https://fresh.deno.dev my-fresh-app && cd $_
deno task start
If this is your first time using Deno see the post on Getting Started with Deno Fresh to get Deno up and running on your system.
Answer the prompts to best suit your own needs. The code below just uses vanilla CSS so there is no need to set up Tailwind support if you are not a fan. Once it is running you can jump to http://localhost:8000
in your browser to see the skeleton project.
⚙️ Setting up the Project for Rust WASM
If you are already familiar with Rust WASM, you might have used the wasm-pack
CLI tool to generate WASM code from Rust. Even though that is not too involved, the Deno wasmbuild
module makes things even easier when using Deno. So we go for the wasmbuild
approach here. When we issued the deno task start
command above this ran the start
script defined in deno.json
in the project root directory. We will update that file to add a new wasmbuild
script:
{
"tasks": {
"start": "deno run -A --watch=static/,routes/ dev.ts",
"wasmbuild": "deno run -A https://deno.land/x/wasmbuild@0.10.2/main.ts"
},
"importMap": "./import_map.json",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
We can use this new script to add a skeleton Rust WASM library to our project:
deno task wasmbuild new
This will create an rs_lib
directory in the project with a Cargo.toml
(analogous to node package.json
file) file and also the Rust skeleton code (rs_lib/src/lib.rs
). We will update these next.
🦀 Rust Code
To make the photon-rs
crate available in the project, we add it to Cargo.toml
:
[package]
name = "rs_lib"
version = "0.0.1"
authors = ["Rodney Johnson <ask@rodneylab.com>"]
edition = "2021"
license = "BSD-3-Clause"
repository = "https://github.com/rodneylab/deno"
description = "Basic image processing"
[lib]
crate_type = ["cdylib"]
[profile.release]
codegen-units = 1
incremental = true
lto = true
opt-level = "z"
[dependencies]
photon-rs = "=0.3.1"
wasm-bindgen = "=0.2.83"
Note that the wasm-bindgen
crate needed to work with WASM in Rust is already listed here. Also worth a mention are the optimisation sensible defaults in the [profile.release]
block (lines 13
-17
). If either performance or size is an issue, you might want to tweak these parameters.
Deno Fresh WASM: Rust Source
Now you can edit rs_lib/src/lib.rs
to look like this:
use photon_rs::{
native::open_image_from_bytes,
transform::{resize, SamplingFilter},
};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
// Use `js_namespace` here to bind `console.log(..)` instead of just
// `log(..)`
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
const PLACEHOLDER_WIDTH: u32 = 10;
#[wasm_bindgen]
pub fn base64_placeholder(data: &mut [u8]) -> String {
let image = match open_image_from_bytes(data) {
Ok(value) => value,
Err(_) => {
console_log!("Unable to open image");
return String::from("");
}
};
let aspect_ratio = image.get_width() / image.get_height();
resize(
&image,
PLACEHOLDER_WIDTH,
PLACEHOLDER_WIDTH / aspect_ratio,
SamplingFilter::Lanczos3,
)
.get_base64()
}
Our main module function, which we will call from Deno is base64_placeholder
defined from line 21
down. Above that we have use statements for the crates and methods we plan to use right at the top. Between the two is a handy macro for creating console.logs. Although originating from the Rust code, these appear in the console when running our code in Deno. Macros kind of create syntactic sugar in Rust. Other examples you might already have seen are format!
, and println!
. You can see an example of invoking our macro in line 26
. Once you have everything working, feel free to stick a “hello world” console_log
on the first line of base64_placeholder
to convince yourself this works.
Moving on, base64_placeholder
takes the image bytes as an input (we will read the image file into a UInt8Array
in the TypeScript code and pass those bytes to this function). Our Rust function uses the image bytes to create an image object. We resize the image so it is 10 pixels wide, preserving the aspect ratio. Finally we convert that output to a Base64 string and return that. Rust has implicit returns, meaning the last line of a function is what gets returned. In our case that will be the Base64 image string.
🔨 Compiling the Module
To compile the module, we can use the wasmbuild
script again. Note you will need to have Rust set up on your system. This uses wasm-pack
under the hood, so as a first step install it, using cargo (if you do not yet have it installed). Cargo is Rust tooling for managing crates (Rust packages). The Rust setup process adds it to your system.
cargo install wasm-pack
deno task wasmbuild
The second step creates a lib
folder in your project (it will be a little slow the first time you run it). The WASM will be in lib/rs_lib_bg.wasm
. This is assembly code which is not human-readable. As well as the WASM you have lib/rs_lib.generated.js
— the JavaScript code we will use import in the next section. If you scan through that file, you will see the base64_to_image
function we created in Rust. Although this is a JavaScript file, it has type information in JSDoc annotations meaning we have types for use in TypeScript in the next section.
Using the Module
Hopefully this hasn’t been too complicated thus far. Using the module is pretty straightforward. We import it (like any other module), then call its asynchronous instantiate
function, and can then use it just like any other import. Here we add a handler to routes/index.tsx
to generate the Base64 placeholder, using our new module:
import { asset, Head } from "$fresh/runtime.ts";
import { Handlers, PageProps } from "$fresh/server.ts";
import Image from "@/islands/Image.tsx";
import { base64_placeholder, instantiate } from "@/lib/rs_lib.generated.js";
import { Fragment } from "preact";
interface Data {
placeholder: string;
}
export const handler: Handlers<Data | null> = {
async GET(_request, context) {
try {
await instantiate();
const data = await Deno.readFile("./content/dinosaur-lemon.png");
const placeholder = base64_placeholder(data);
return context.render({ placeholder });
} catch (error: unknown) {
console.error(`Error doing stuff with image file: ${error as string}`);
return context.render(null);
}
},
};
Note in line 16
, we read the image file from disk as binary data into a data
which is a UInt8Array
. That is all there is to it!
That block above uses a path alias to reference files within the project. You can set this us by updating import_map.json
{
"imports": {
"@/": "./",
"$fresh/": "https://deno.land/x/fresh@1.1.2/",
"$std/": "https://deno.land/std@0.171.0/",
"preact": "https://esm.sh/preact@10.11.0",
"preact/": "https://esm.sh/preact@10.11.0/",
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@5.2.4",
"@preact/signals": "https://esm.sh/*@preact/signals@1.0.3",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.0.1"
}
}
We also sneakily added in a line using the Deno std library above, so be sure to update line 5
too!
Here is the rest of the home route code and also the Image
Preact component which we reference:
export default function Home(context: PageProps<Data | null>) {
const { data } = context;
if (!data) {
return (
<Fragment>
<div>Something went wrong!</div>
</Fragment>
);
}
const { placeholder } = data;
return (
<>
<Head>
<title>Deno Fresh 🍋 Rust WASM</title>
<link rel="stylesheet" href={asset("/fonts.css")} />
<link rel="stylesheet" href={asset("/global.css")} />
<link rel="icon" href={asset("/favicon.ico")} sizes="any" />
<link rel="icon" href={asset("/icon.svg")} type="image/svg+xml" />
<link rel="apple-touch-icon" href={asset("/apple-touch-icon.png")} />
<link rel="manifest" href={asset("/manifest.webmanifest")} />
<title>Deno Fresh 🍋 Rust WASM</title>
<meta
name="description"
content="Deno Fresh WASM: how you can code your own WASM modules in Rust 🦀 and easily integrate them into your Deno Fresh 🍋 project."
/>
</Head>
<main className="wrapper">
<h1 className="heading">FRESH!</h1>
<Image
src="/content/dinosaur-lemon.png"
placeholder={placeholder}
alt="Cartoon style image of a dinosaur wiht a mouth full of lemons"
/>
</main>
</>
);
}
import { useEffect, useRef } from "preact/hooks";
interface ImageProps {
alt: string;
placeholder: string;
src: string;
}
export default function Image({ alt, placeholder, src }: ImageProps) {
const imageElement = useRef<HTMLImageElement>(null);
useEffect(() => {
if (imageElement.current) {
imageElement.current.src = src;
}
}, [src]);
return (
<figure className="image-wrapper">
<img
ref={imageElement}
alt={alt}
loading="eager"
decoding="async"
fetchPriority="high"
height="256"
width="256"
src={placeholder}
/>
</figure>
);
}
🙌🏽 Deno Fresh WASM: Wrapping Up
In the post we have had a whistle-stop tour of setting up your first Deno WASM project. In particular, we saw:
- how to use wasmbuild to quickly add a Rust WASM module to your Deno project,
- how to add console logs in your WASM Rust code,
- some basic Rust image manipulation.
The complete is in Rodney Lab GitHub repo. As a next step you might consider publishing your WASM module to deno.land/x so other developers can easily use it. This is something I did with the parsedown module. It has code I use for the Newsletter and parses Markdown to HTML as well as generate email HTML and plaintext emails. Let me know if you would like to see a short video on publishing a Deno module.
If you want to learn more on wasm-pack, there is a wasm-pack book as well as some fairly detailed wasm-bindgen
docs. There are a few resources for learning Rust itself in the December newsletter. Finally, please get in touch if you would like to see more content on Deno and Fresh. I hope you found the content useful and am keen to hear about possible improvements.
🙏🏽 Deno Fresh WASM: Feedback
Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, then please consider supporting me through Buy me a Coffee.
Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter, @rodney@toot.community on Mastodon and also the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as SvelteKit. Also subscribe to the newsletter to keep up-to-date with our latest projects.
Top comments (0)