The dream of running native code in the browser is not something new. There were many failed attempts. They all taught us a lesson. Those learnings made WebAssembly
possible today.
WebAssembly makes it possible to run languages like C, C++, Rust and other languages in the browser.
Check out my book on Rust and WebAssembly here
But what is WebAssembly? Check out this presentation here or this awesome post from Lin Clark.
TL;DR:
- Rust's toolchain makes it easy write WebAssembly application.
- If you want better performance then use
opt-level=3
. - If you want a smaller sized bundle then use
opt-level="s"
.
What are we gonna do?
Create a WebAssembly application that takes a string in markdown
format and converts that into HTML.
Lets get started
So far, Rust has the best tooling for the WebAssembly. It is well integrated with the language. This makes Rust the best choice for doing WebAssembly.
We will need to install Rust before getting started. To install Rust checkout the installation guide here.
Once you have the Rust installed. Let's start creating the application.
Create Application
Create a WebAssembly application with all the necessary toolchain:
npm init rust-webpack markdown-rust
This creates a new Rust + JavaScript based application with Webpack.
Go inside the directory
cd markdown-rust
It has both Cargo.toml
and package.json
.
The Rust source files are present in the src
directory and the JavaScript files are available in js
directory. We also have webpack configured for running the application easy and fast.
The Cargo.toml
contains the following:
[package]
# Some package information.
Then it declares the project will build a dynamic library
with the following command.
[lib]
crate-type = ["cdylib"]
We have also declared the release profile should optimize the release using lto
flag.
[profile.release]
lto = true
Finally added some [features]
and [depdencies]
.
Now all We have to do is add the markdown
library for the Rust that compiles the Markdown (string) into HTML string.
[dependencies]
# some comments ......
wasm-bindgen = "0.2.45"
comrak = "0.6"
Remove all the contents from src/lib.rs
and replace that with the following.
Load the comrak
functions and wasm_bindgen
that we will be using.
use comrak::{markdown_to_html, ComrakOptions};
use wasm_bindgen::prelude::*;
So what is wasm_bindgen
?
WebAssembly does not have any bindings to call the JavaScript or Document APIs. In fact, we can only pass numbers between JavaScript and WebAssembly. But that is not always desirable right, we need to pass JS objects, Strings, classes, closures and others between them.
How can we achieve that?
We can create a binding file or glue file that helps to translate the above objects into numbers. For example, in case of the string rather than sending each character as a character code.
We can put that string in a linear memory array and then pass the start-index (of where it is in memory) and its length to the other world (or JavaScript). The other world should have access to this linear memory array and fetches the information from there.
But doing this for every value that we pass between JavaScript and WebAssembly is time-consuming and error-prone. The wasm_bindgen tool helps you to build the binding file automatically and also removes the boilerplate code with a single #[wasm_bindgen]
annotation.
But we need to be very careful about how many times we cross the boundary between JavaScript and WebAssembly module. More we cross slower the performance will be.
Now we will create a function called parse that actually takes the markdown input and returns the HTML.
#[wasm_bindgen]
pub fn parse(input: &str) -> String {
markdown_to_html(&input.to_string(), &ComrakOptions::default())
}
The #[wasm_bindgen]
annotation does all the boilerplate of converting the string into two numbers, one for the pointer to the start of the string in the linear memory and the other for the length of the string. The #[wasm_bindgen]
also generates the binding file in JavaScript.
Time for some JavaScript β€οΈ
Now we have the WebAssembly Module ready. It is time for some JavaScript.
We will remove all the lines from the js/index.js
and replace that with the following contents.
We will first import the WebAssembly module generated. Since we are using Webpack, Webpack will take care of bootstrapping wasm_pack
that will, in turn, use the wasm_bindgen
to convert Rust into WebAssembly module and then generate the necessary binding files.
The wasm_pack
is a tool that helps to build and pack the Rust and WebAssembly applications. More about Wasm-pack here.
This means we have to just import the pkg/index.js
file. This is where wasm_pack will generate the output.
const rust = import('../pkg/index.js');
The dynamic import will create promise which when resolved gives the result of the WebAssembly modules. We can call the function parse
defined inside the Rust file like below.
rust.then(module => {
console.log(module.parse('#some markdown content'));
});
We will also calculate the time it took to parse the contents using the WebAssembly module.
rust.then(module => {
console.log(module.parse('#some markdown content'));
const startWasm = performance.now();
module.parse('#Heading 1');
const endWasm = performance.now();
console.log(`It took ${endWasm - startWasm} to do this in WebAssembly`);
});
For comparison, we will also calculate the time it took to do it with JavaScript.
Install the markdown library for the JavaScript.
npm install --save marked
Once installed, let us write our JavaScript code that takes in a Markdown text and returns the HTML.
// js/index.js
import marked from 'marked';
// some content goes here;
const markdown = '#Heading';
const startJs = performance.now();
console.log(marked(markdown));
const endJs = performance.now();
console.log(`It took ${endJs - startJs} to do this in JavaScript`);
Let us run the application using npm run start
. This will kick start the Webpack dev server and serve the content from the local.
It is quite an interesting performance statistics to look at.
In Chrome and Safari, the JavaScript performance is way better than the WebAssembly. But in Firefox the JavaScript version is 50% slower than the WebAssembly.
This is mainly because WebAssembly linking and bootstrapping is very very fast in Firefox than compared with any other browser.
If you take a look at the bundle size, the WebAssembly file is mammoth 7475 KB than compared with the JavaScript variant 1009 KB.
If you are booing for WebAssembly now, then wait.
We did not add any optimizations yet. Let us add some optimizations and check the performance.
Open the Cargo.toml
file and add the following segment above the [features]
section.
[profile.dev]
lto = true
opt-level = 3
The opt-level
is nothing but optimization level for compiling the project.
The lto
here refers to link-time-optimization
.
Note: This optimization level and lto should be added to the
profile.release
while working on the real application.
Additionally, enable the wee_alloc
which does a much smaller memory allocation.
Uncomment the following in the Cargo.toml
[features]
default = ["wee_alloc"]
Add the wee_alloc
memory allocation inside the src/lib.rs
file.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
Now let us restart the server.
We can now see the real performance benefits of the WebAssembly.
In Chrome the WebAssembly version is 4 times faster than the JavaScript version.
In Safari, the JavaScript variant is still between 2-3 ms but the WebAssembly variant is between 0-2ms.
Firefox too saw almost 50% faster WebAssembly code when using the optimizations than without optimizations.
Now the all-important bundle size is 1280 KB for WebAssembly and 1009 KB for JavaScript.
We can also ask Rust compiler to optimize for size rather than speed. To specify that change the opt-level
to s
opt-level = "s"
WebAssembly still is a clear winner, but the Chrome registers slightly increased WebAssembly times but still lesser than the JavaScript variant. Both Safari and Firefox provide higher performance for the WebAssembly.
The bundle size is reduced further for WebAssembly at around 1220 and 1009 KB for JavaScript.
Rust compiler also supports opt-level = "z"
which reduces the file size even further.
opt-level = "z"
The bundle size is reduced further for WebAssembly at around 1161KB and 1009 KB for JavaScript.
The performance of the WebAssembly module in Chrome is fluctuating a lot when using opt-level='z'
between 41 and 140 ms.
IE Canary for Mac has (~)almost the same performance as of Chrome.
Use opt-level="z"
if you are more concerned about your bundle size but the performance is not reliable in v8 now.
I hope this gives you a motivation to kick start your awesome WebAssembly journey. If you have any questions/suggestions/feel that I missed something feel free to add a comment.
If you have enjoyed the post, then you might like my book on Rust and WebAssembly. Check them out here
You can follow me on Twitter.
If you like this article, please leave a like or a comment. β€οΈ
Top comments (10)
How long should it take to bundle? I've been waiting for a really long time... like four minutes, and it still says "wait until bundle finished:"
Okay, it took ~10 minutes, but once it got going, it was able to live-reload in a few seconds.
Here's the JS I used:
And the result (Safari 12.11):
Hold up, I'm going to update my rust and add the optimizations and see what sizes and times we get.
Okay, updated them and compiled with opt-level=3, and wee_alloc on. Compilation took a really long time, but I get that it's a prod feature, not a dev feature. Wasm file was 1.3Mb, though :/
I tried compiling with
s
andz
to see the file size difference, but it failed to compile when I tried that (it deleted thepkg
directory and then didn't regenerate it)Anyway, good article, thanks for writing it up :)
What was the error message when you did
s
andz
? You should pass it as a String.All the above tests are run on rustc
1.37.0-nightly (929b48ec9 2019-06-21)
WASM file size is around that value, this may change in the future. π€
Oh, that's probably what I did wrong, I just had
opt-level=s
, notopt-level="s"
The error wasn't interesting, it was about how it tried to import the index, but the file wasn't there. I'm 90% sure it originated in the browser and that the server was just echoing it for me to see. Other than that, it just said "build failed". Which is interesting, because when I set it to
opt-level=5
, it told me that the only valid values were 1-3, and s and z, so it's a bit surprising that I didn't get that error again).It is installing wasm-pack and then wasm bindgen cli. If this the first time, then it might take sometime. Try installing manually and check if it fails. Check out wasm-pack docs for this. successive runs will be faster
So I have been tinkering with this mad project, and now I have learned that c++ is very verbose, do you find rust easier to write, manage and understand. I'm contemplating version 2 in rust with rlua.
acronamy / tidal-node
Node.js tidal is my experiment to transparently integrate Lua and Lua rocks (TODO) into a simple node library via WASM. *optionally* Want to require Lua scripts from Node and have it give you a table? Want to use node features in Lua?
Tidal Node
Lua was concieved as an embeded language, designed to compliment other languages, Tidal Node is a library which brings Lua to node.js through WebAssembly and c++.
Tidal Node is WIP and not production ready - however I welcome the one or two people in the world that want to use this to get those PR's coming in. You will find most of the emscripten compiler commands in npm package.json "config". There is also a installer task for Lua built in, everything is subject to change.
Requirements
compiler:
emscripten 1.38.30^
runtime:
node 10.0.0^
build tools:
Commands
What is working
Highlights - "It's alive!"
Thats a great idea ππ Yeah Rust is very concise. When you start you have wrap your head around the ownership, lifetimes and others. But when you get the feeling for that, then it will be much more easier and simpler to write.
Thank you for your kind words. So there's no emscripten, sounds interesting! Okay well I'm gonna carry on getting this version done and rolled out before giving this a shot.