About me: over the last year-ish I’ve been busy building the web app bloom3d.com and the game-engine that powers it. Bloom and the engine are around 96% Rust which gets compiled to WebAssembly.
I’ve also built a Wasm / SIMD-powered 3D terrain generator: https://ianjk.com/terrain_generator/
And I’ve made three game jam entries with Rust / Wasm:
In this article I address the question: should your web app use WebAssembly?
For those unfamiliar with WebAssembly: WebAssembly (abbreviated to Wasm) is a way for languages like Rust / C / C++ / Zig / etc. to run securely on the web. Wasm is also seeing some use outside of the web as a safer way to run high-performance code.
I’ve read a bunch of misconceptions about when you should and should not use Wasm and I wanted to take a moment to share my thoughts on the topic.
Don’t use WebAssembly primarily for performance
In my opinion the biggest mistake people make is assuming they should use Wasm because it will make their website faster. That is not a good reason to use Wasm!
- Avoiding garbage collection overhead
- Better memory-layout (it’s faster to iterate memory that’s stored in a row)
- It can be easier to reason about control-flow
- SIMD code
But the typical web-app isn’t going to be bottlenecked on the sort of algorithms that would benefit from those advantages.
Don’t use WebAssembly for document-style website UI
Most Wasm languages don’t have user-interface libraries as robust as those for the web. If you go with a Wasm-based UI you’re likely going to end up with worse accessibility, search-engine optimization (SEO), performance, internationalization, and standard UX.
If you go full Wasm canvas UI for a website that’s mostly a document you’re abandoning years of nuanced browser API development. Flutter is trying the all-Wasm UI approach but many people dislike it.
However if you are building a web app it’s likely that the user will already expect your application to work less like a document so there’s more leeway with user expectations.
Makepad achieves incredible results with their custom UI stack, but at the expense of internationalization and accessibility.
Websites like Figma use Wasm for some core app logic and regular HTML / CSS for much of the UI. For many web apps this is a great compromise. It’s only worth using a Wasm canvas based UI if you have unique goals.
Why should you use WebAssembly?
Using Wasm for a web app can have serious advantages:
- Consistent performance
- One language can be used everywhere (server, mobile, etc.) regardless of performance constraints
But in general I find Rust / Wasm easier to attain consistent great performance with and easier to diagnose performance problems for.
With low-level languages like Rust / C++ / C there are a few key things to avoid to attain great performance:
- too many memory allocations
- scattered memory layouts
- deeply nested loops
I try to keep those in the back of my head when writing code and rarely I’ll have to revisit some code to improve performance in a targeted location.
Similarly it can be a huge performance boost to arrange accessed data all in a row in memory. I never get too analytical about this but in general I try to avoid data structures like graphs where each node is a separate allocation.
With Wasm languages it’s easier to write well performing code by default and you have extra capabilities to improve performance where necessary.
One language everywhere
Most web apps are web apps and they don’t need to be something else, but in some cases you may want to offer a native version of their app with true native performance.
For me writing bloom3d.com in Rust leaves open the possibility of a native-performance virtual-reality port to the Quest (likely not happening any time soon). Or I could write a native-performance multiplayer server and share most of the code-base with the client.
I never have to worry about feeling constrained by my chose of language. Rust can confidently run everywhere!
I could even use some of my Rust code to write a game for a super low-power platform like Playdate.
With the code base I’ve built in Rust I’m confident I can use it for almost any future project on web or otherwise. It’s a unique strength that means I’m not beholden to any one platform’s fate.
You get to use Rust and its native libraries
This point is a bit tailored to my tastes but it’s a big part of why I use WebAssembly.
I like Rust. It’s a great language that builds upon learnings from past languages and makes it easier to write significant software projects. Rust isn’t perfect but by my assessment it’s the best choice for building great software today.
By using Rust / Wasm I can use all sorts of native Rust libraries that were built by a community that often cares deeply about performance and correctness.
I am extremely choosey about what libraries I use but I’ve had great success integrating libraries like
oddio for sound mixing / spatialization and
fontdue for text rendering.
WebAssembly can’t actually interact with the browser without going through JS so Wasm needs to send a message to some tiny JS to do anything of substance.
When creating bloom3d.com I never measured any significant overhead in such calls but just in case I (prematurely?) optimized the chattiest JS ↔ Wasm part of the app. The game-engine powering bloom packs all of its graphics related commands into a list and then calls JS to pass it the list. Then the JS iterates over the commands by directly reading the Wasm’s memory.
No copies, a single Wasm ↔ JS message, and good performance!
I suspect most web-apps won’t need to make optimizations like that, but the option is always there if it becomes a problem.
wasm-bindgen that lets you write Rust code that calls the equivalent JS behind the scenes.
wasm-bindgen works fine but when interacting with things like JS promises it becomes clear that Rust isn’t made to be ergonomic in many common JS cases.
I wrote my own abstraction to interact with JS APIs. It builds faster and offers more control than
wasm-bindgen but it’s not as robust and relies too much on
eval. So I’m not delighted with the ways to communicate between JS and Rust / Wasm, but it’s not slow and it’s a small portion of the app's total code.
What types of web apps should consider WebAssembly?
There are a few types of web-apps that stand to benefit more from WebAssembly:
Web apps with many low-level algorithms
Apps like image/audio/video editors or 3D modeling tools all rely on low-level algorithms that directly modify large amounts of data. This is what low-level languages are great at! By picking Wasm for such tasks you’ll likely find it easier to port existing low-level algorithms or to even use existing libraries. And performance will be very predictable and easy to tune.
Web apps with highly complex 3D scenes
If you’re making something like a game with lots of moving parts Wasm may benefit you. Many of the algorithms used in game-engines fit nicely with Wasm’s strengths. Things like Entity-Component Systems, physics engines, culling, pathfinding, etc. all benefit from Wasm’s performance characteristics.
Today this is quite niche because there aren’t many great Wasm-backed game engines. Unity can target in Wasm but it’s slow to load, build, and run. (needle-tools may be improving that)
But given that the browser tech for a Wasm-native game engine is there I suspect we’ll see a few strong contenders emerge. Or you could build your own Wasm game engine like me but I don’t recommend that unless you’re willing to invest a bunch of time!
Web apps that may also run on native
It’s possible to use Electron to port a web app to native, but perhaps you desire native performance or a smaller memory / storage footprint.
If you’re using a Wasm language a true native port is possible.
Web apps built by those who prefer Rust / C / C++
If you’re part of an organization that already uses Rust / C / C++ and you’d like to make a web app you may find it convenient to share code and engineers with your web app.
Web apps with user plugins
This is niche but worth calling out. Wasm is easily sandboxed and controlled. This makes it a good candidate for apps that allow users to write and use “plugins” that run untrusted code.
Imagine something like a music composition app that allows user-created plugins for different instruments.
AudioWorklets are a modern web API that are probably the best way to do custom audio mixing and playback in the browser. However it’s very important to never stall an AudioWorklet as that can lead to audio distortions.
Wasm makes it much easier to avoid any garbage collections or accidentally slow operations that could stall the worklet and introduce audio glitches.
In the future: Heavily multithreaded code
If you use Rust and multiple webworker threads backed by
SharedArrayBuffer where a Wasm language like Rust doesn’t need to do any of that at all.
Unfortunately this isn’t seamless in Rust. There are obstacles to making Wasm multithreading seamless in Rust and progress on it seems to have slowed. See this GitHub issue: https://github.com/rust-lang/rust/issues/77839
It’s possible to make Rust / Wasm multithreading work but is an effort and there isn’t a standard approach.
But more broadly the Wasm multithreading story isn’t great.
SharedArrayBuffers need to be declared with a fixed upper memory size and I have found it variable what size browsers will accept. There’s more discussion of the pitfalls in this thread: https://github.com/WebAssembly/design/issues/1397
Another pitfall that can lead to less-than-optimal performance is the main thread is not allowed to block like a native thread would be. Most of the time this isn’t an issue but this forces Rust threads to use a spin-lock when accessing the global memory allocator. I suspect this leads to degraded performance in scenarios with high contention between threads. It’s one of those frustrating web things: browser devs prohibit blocking the main thread so a spin-lock needs to be used instead which is likely worse than a simple block. Oh well.
Safari also has an outstanding bug that makes multithreaded communication with an AudioWorklet impossible: https://bugs.webkit.org/show_bug.cgi?id=220038
In the future: Code that benefits from SIMD optimizations
This is a unique strength Wasm has that JS does not. Wasm can tap into SIMD which can unlock significantly better performance in certain cases.
See a description here: https://v8.dev/features/simd
Unfortunately this is not yet implemented in Safari and it’s unclear when they’ll implement it.
And I’ve encountered subtle bugs in Chrome that leave me wondering if I’m the only one in the world using Wasm / SIMD: https://bugs.chromium.org/p/chromium/issues/detail?id=1313647
Yes, some web apps should seriously consider using WebAssembly. Especially for new browser apps there are major advantages.
But many traditional web apps will not benefit from switching to WebAssembly.
A few links with related opinions that motivated me to write this:
Is WebAssembly magic performance pixie dust?
This Twitter thread about why you shouldn’t use Wasm