Experimenting with Rust and WebAssembly

Published: Sun Apr 19 2020

Today I decided to run a quick experiment with Rust and WebAssembly. Here is a write up of what I learned.

Rust and WebAssembly

Until this morning I had never looked at Rust, but after experimenting with Balzor and WebAssembly in a previous article, I was curious about how Rust’s offering compares to the .Net Blazor implementation.

On the surface Blazor and Rust are similar in that they both compile to wasm, but one key difference is that Rust doesn’t come with a huge runtime dependency like Blazor does. This results in a much more nimble application with a much smaller footprint. As a result I can definitely see a relatively big difference in startup time between the two. If you read my Blazor article you may remember that I used Blazor to create a simple bio section for my blog. Instead of repeating the same exact experiment, I decided to use Rust to create a “recent article” section for my blog.

Demo

The Recent article section is fairly simple. Basically all it does is fetch a list of recent articles and stamp out an html view on the page.

Let’s take a look at the Rust code:

#![recursion_limit = "256"] use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::{Request, RequestInit, Response}; extern crate stdweb; extern crate typed_html; use stdweb::web::{self, INode}; use typed_html::dom::Node; use typed_html::html; use typed_html::output::stdweb::Stdweb; use typed_html::text; #[derive(Debug, Serialize, Deserialize)] pub struct Article { pub key: String, pub intro: String, pub title: String, } #[derive(Debug, Serialize, Deserialize)] pub struct ArticleItem { pub html: String, } #[wasm_bindgen] pub async fn render_article_list() -> Result<JsValue, JsValue> { let mut opts = RequestInit::new(); opts.method("GET"); let url = "/some-api-url"; let request = Request::new_with_str_and_init(&url, &opts)?; request.headers().set("Accept", "application/json")?; let window = web_sys::window().unwrap(); let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; let resp: Response = resp_value.dyn_into().unwrap(); let json = JsFuture::from(resp.json()?).await?; let article_info: Vec<Article> = json.into_serde().unwrap(); let mut doc = html!( <div class="container"> <h1>"Most Recent Articles"</h1> { (article_info.into_iter()).map(|a| html!( <div class="article-section"> <h5> <a class="article-link" href={String::from(format!("viewarticle/{}", a.key))}> { text!("{}", a.title) } </a> </h5> <p> { text!("{}", a.intro) } </p> </div> )) } </div> : Stdweb); let vdom = doc.vnode(); let document = web::document(); let body = document.body().expect("no body element in doc"); let tree = Stdweb::build(&document, vdom).unwrap(); body.append_child(&tree); let item = ArticleItem { html: String::from(doc.to_string()), }; Ok(JsValue::from_serde(&item).unwrap()) }

Rust definitely has a learning curve to it, mostly because of syntax that I’m not used to as well as a new package manager system called crates.

For application bundling I opted to go with Rollup combined with the @wasm-tool/rollup-plugin-rust plugin.

Rollup outputs two files; a .js bundle file and .wasm file that will be loaded by the browser at runtime. For performance reasons I put both of these files behind my CDN.

Blazor vs Rust

Now that I have two WebAssembly features deployed as part of my blog it might be fun to compare the load times of both.

The Blazor component is a tad more complicated since it includes routing, but in principle both features should be comparable.

If you are interested in comparing performance, I have included the links to both below:

Blazor - bio

Rust – recent articles

In both cases I am loading all file dependencies from a CDN, but the large payload size of the Blazor implementation leads to a much longer startup time. That said, you only pay the startup tax on the first request. Subsequent requests will load the .Net dependencies directly from browser cache.

I think Blazor will reduce the payload over time, but at the time of writing the gap is almost a little bit insane at 92K for Rust and 3.3MB for Blazor after gzip compression.

Follow me on twitter @MoreTechStories