I’ve been using WebAssembly aka WASM for a while in production to do some incredible stuff in the browser, things that would be prohibitive in terms of performance otherwise. Here are some real use-cases I’ve used WASM for:
- Running statistical processing 10 million rows (roughly 50 columns in each row) of CSV data in browser. This feature required us to create a temporary playground of reports generated for our clients where they could run their own analysis without the need of permanent storage or costly servers.
- Parsing DOCX and PPTX files in browser and running DL Models on the data in browser. We saved thousands of $ by eliminating the persistent backend and reduced the time to result from minutes to a few seconds.
- One use-case ended up having tons of complex user input validations, we were building these validators on the server-side anyway, and with WASM we just put the same validator code in the client improving productivity and minimizing chances of mistakes and discrepancies between client and server validators.
- Audio-Video encoding in browser, saving time and thus making content generation more dynamic and open to experimentation.
And a few more …
Since you are here you’ve probably already heard of or read about the success stories of Figma, Adobe has deployed their browser based Acrobat, Lightroom etc. using WASM, Google uses WASM in countless products and plenty more. WASM also finds it’s uses beyond the browser - Shopify Functions, Cloudflare Workers and many more.
In this post, I’m going to focus on WASM in Browser by building a small text-diff
application with Rust.
TL; DR
The Final Output
What is WASM?
I think Mozilla provides us with the best introductory definition:
WebAssemblyWebAssembly is a type of code that can be run in modern web browsers — it is a low-level assembly-like language with a compact binary format that runs with near-native performance and provides languages such as C/C++, C# and Rust with a compilation target so that they can run on the web. It is also designed to run alongside JavaScript, allowing both to work together.
-— MDN
And my nutshell version
WASM brings the power of low-level programming languages to the browser environment. That’s it!
When to use WASM?
It’s important to note that WASM is not a replacement for your current web app stack. JavaScript, HTML, CSS are the native standards, and they are your primary building blocks for your web-app. Think of WASM as a tool to augment your browser stack or a tool to eliminate bottlenecks.
Here are my top 3 reasons of using WASM:
- Performance: If your application’s Data Processing far outweighs DOM Manipulation, WASM will help.
- Portability: If you use a bunch of battle-tested libraries but their JS port is not available, not as performant or not feature-complete, WASM might help
- Edge Compute: This has been the most common trigger for my WASM usage. If your application does a bunch of stuff that would require backend-level programs, but the client-server communication latency is a major limiting factor to the experience; or a specific feature is not used frequently enough to justify a backend server deployment - WASM probably would be the right tool for you.
How to use WASM?
Ok, enough with the rationale and explanations let’s dive into building something real. We are going to be creating a simple web-application to display a diff
between two text blocks and hopefully in this process see some performance benefits. We are going to write a simple Rust
program to do the diffing
in WASM while rest of our interface will be in HTML + JS
.
Setup
Let’s start by creating a basic Webpack project.
mkdir wasm-diff
cd wasm-diff
npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin nodemon
npm i -D tailwindcss
npm i -D style-loader css-loader postcss postcss-loader postcss-preset-env
npx tailwindcss init
In the first 3 commands we initialized a basic npm
project, the 4th line is webpack
related stuff. The 5th, 6th and 7th lines are for tailwindcss
library, I just HATE working with vanilla CSS.
Configuring TailwindFollow these instructions for configuringtailwindcss
for our project. This is optional and vanilla CSS will work just fine.
|
|
Now, let’s wrap up our working app
with a working src/index.js
file.
|
|
Let us run our app now to see if everything is in order.
npm run dev
Navigate to http://localhost:8080
in your browser and you should see the app running.
That should complete our setup! Let’s move on to getting the diff
to work.
Diff with JavaScript
We are going to be using diff-match-patch
npm package for our JavaScript diff, let’s install it.
npm i diff-match-patch
The interface is simple, we’ll use the diff_main
API of the package to create the diffs
of our old
and new
texts and then use the example diff_prettyHtml
API to create the HTML string and display it.
Let’s change our src/index.js
to do that.
|
|
We initialized the JavaScript DiffMatchPatch
class, and in our jsDiff()
function which is called by onclick
event listener, we computed the diff, cleaned it up a bit and put the results and the stats
in their respective DOM containers.
Note the diff time, in my current run it took about 512 ms to generate the diff. Let’s see if diffing with a WASM
module adds any performance advantage to it or not!
We are going to compile a rust
library to WASM
, but if you are comfortable with C++
, Go
or C#
, this will work just as well. Each one would have their own nuances to handle but that’s a story for another day!
Getting ready to WASM
Let’s start by creating a new rust project to house our WASM
code, I’m creating an unimaginative wasm
directory, it could be anything. We’ll use wasm-pack
tool to help us with the build process.
PrerequisitesRust toolchain
Assuming you have rust
and cargo
toolchain installed, let’s key in the commands.
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
mkdir wasm
cd wasm
cargo init --lib
The first command installs the cargo extension wasm-pack
, through command 2 to 4 we are just creating a rust project of type --lib
, it’ll not contain a main()
function.
To ready our wasm
crate let’s go ahead make some changes to wasm/Cargo.toml
|
|
The wasm-bindgen
crate we added to our [dependencies]
section provides us with the batteries required to interface with JS
and browser
modules. The [lib] crate-type
declaration basically tells our compiler to generate dynamic system library. This is used when compiling a dynamic library to be loaded from another language.
We’ll also need to modify our webpack.config.js
to enable loading the generated wasm
files.
|
|
By adding the declaration experiments.asyncWebAssembly: true
we are telling webpack
to load wasm
modules asynchronously.
As a final step of the setup, let’s go ahead and add the build command to our package.json
file to compile and generate the wasm
modules and put it in our src/pkg
directory.
|
|
Let’s break down the dev
command wasm-pack build wasm --out-dir ../src/pkg --target web && webpack serve --config webpack.config.js
wasm-pack build
- thecargo extension
we installed earlier, we are using it to generatewasm
modules from ourrust
librarywasm
- tellingwasm-pack
that ourrust
library code resides in<PROJECT_ROOT>/wasm
directory--out-dir
is tellingwasm-pack
to put the generated output files in<PROJECT_ROOT>/src/pkg
directory--target web
is tellingwasm-pack
to build for the browser. You could write awasm
library for sayNodeJS
environment, this option will change&& webpack ...
- rest of the command is basically to run the dev server we have already been running
That concludes our setup, if we run npm run dev
now, we should see a bunch of files generated under the src/pkg
directory.
But wait, nothing really happens yet! We have not written any code to call or do anything with the WASM
module we just generated. Let’s work on that!
Hello World
from WASM
We are going to write a function in our rust
library return a Hello World
string, and then we are going to call it from our JavaScript
code.
Let’s modify our wasm/src/lib.rs
file to return the Hello World
.
|
|
The code is simple:
- the
wasm-bindgen::prelude::*
imports the batteries required to generate wasm modules and helpers that enable ourrust fn
or code to be called from JavaScript #[wasm_bindgen]
macro tells the compiler that this function needs to be exposed for an external interfaceconsole_error_panic_hook::set_once();
is initializing a globalhook
so that if ourwasm
code generated fromrust
panics, aconsole.error
would be thrown
Rest of it is basic rust
code, just returning a Result<String>
, Hello from Wasm!
in this case.
Now, let’s add a couple of lines to our src/index.js
file to call this function.
|
|
Basically we are importing the wasm initializer
and initializing the wasm module, once the initializer resolves we are simply calling the run()
function and printing its output.
Let’s run it .. if everything goes well, we should see Hello from Wasm!
in our console!
npm run dev
And there you go, our first real stuff from WASM
…
Diff with WASM
Let’s pick up a rust crate
to do our Diff
, there are a few options available if you search crates.io with the keywords diff match patch
but as far as I know none of them support WASM
out of the box. For this example, I’m going to use my own UNPUBLISHED crate diff-match-patch
for the WASM
diffing.
Pro TipsNot allrust crates
would beWASM compatible
for various reasons. A sure-shot way of figuring this out is to navigate todocs.rs
page for thecrate
and look through thePlatforms
tab. Most mature crates would showwasm32-unknown-unknown
if they areWASM
ready. The other way is to clone or use the crate in a toy project and usecargo check --target wasm32-unknown-unknown
to look for errors. Some Crates might also enableWASM
withfeature
flags.
Let’s add diff-match-patch-rs
to our dependencies in wasm/Cargo.toml
.
|
|
Now, let’s change our wasm/src/lib.rs
to do the actual diffing.
|
|
NoteWe don’t REALLY need to declare a separatestruct Differ
for our relatively simple use-case here, but often you’ll need to create an instance of a struct and access their methods. This is just to demonstrate the possibilities and introduce a few handy concepts.
Let’s break down what’s happening here.
line 4-8
we are declaring astruct Differ
and annotating it with the#[wasm_bindgen]
macro, basically telling the compiler that instance of this struct will be accessed fromforeign code
.line 10-11
we are annotating theimpl Differ
block with#[wasm_bindgen]
- in a more complex usecase we could have otherimpl Differ
blocks which will not be accessed from ourJavaScript
, we’ll just skip#[wasm_bindgen]
for thoseimpl Differ
blocks.line 12
is interesting, here we are using the#[wasm_bindgen(constructor)]
macro to tell the compiler that thepub fn new()
should be called as a constructor if we donew Differ()
from ourJavaScript
code.
The rest of it serves the exact same flow to our JavaScript
diff function call, it calls the diff_main()
method of diff_match_patch_rs::dmp::DiffMatchPatch
module instance and then created the html
string and returns it.
Let’s modify our src/index.js
to make use of this WASM
diff code.
|
|
Let’s run it now …
npm run dev
And, then, lets browse to localhost:8080
and click the Run JS Diff
and Run WASM Diff
buttons …
🥁🥁🥁 … drum rolls …
WHOA, the WASM variant is 80% faster than the JavaScript version.
Closing thoughts & Next steps
80% speedup is a LOT of performance gain. In a static setup like our current version, it may not make a difference but imagine you are writing a collaborative editing tool
like Google Docs, and you need to diff
on every keyup
… the performance gain we saw is going to be the difference between an usable and an unusable product.
Here are some ideas you can explore based on what you’ve done today:
WASM modules tend to be larger in size than your standard JavaScript variant. But good news is, there are ways of optimizing it. Read up about those optimizations (and easier ways of setting up WASM) in the
rust wasm book
. Here’s the chapter on specific size optimizations.Modify the code and apply the
patch
to the source text. Hint: You’ll need to figure out a way of sendingJsValue
instead of plainString
we’ve been using so far.
A word of cautionIf you are using mydiff-match-patch-rs
crate, be careful. It’s still a work in progress and I’ll probably work on optimizations soon.
- Convert this application to a
real-time diff
machine.
Before we close today …
If you have found this tutorial helpful consider spreading the word, dropping a star to the git repo or the crate repo it would act as a strong motivator for me to create more. If you found an issue or hit a snag, reach out to me @beingAnubhab. I’d love to hear about stuff you did with what you learnt today.
Acknowledgements, references and further reading
diff-match-patch
is based onMyer's Diff
algorithm, which has been a standard since 1989, how cool is that! This specific version we worked on is based ongoogle/diff-match-patch
library; the original source code can be found here.- We used
wasm-bindgen
which is a rust library and CLI tool that facilitates interactions betweenWASM
modules andJavaScript
code. Like many things in Rust, it’s well documented and there’s abook
for that. wasm-pack
makes our life a lot easy when we work withRust WASM
and the book gives a nice and easy to follow walkthrough.- If you are as fascinated by
WebAssembly
as I am, and you are looking for the next cool project idea - browse through made with webassembly.