WebAssembly vs Javascript

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!

Wasm can be faster, but not always. And Javascript can be exceptionally fast!

If you’re a Javascript web dev developing a web app and you hit a performance bottleneck it’s likely more worthwhile to figure out how to optimize the Javascript instead of replacing it with Wasm.

However WebAssembly can outperform Javascript in a few ways:

But the typical web-app isn’t going to be bottlenecked on the sort of algorithms that would benefit from those advantages.

Introducing Wasm as a targeted optimization into a primarily Javascript codebase will generally add unnecessary complexity.

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

Javascript is tremendously optimized for what it is and its higher-level nature gives the browser more wiggle-room for clever optimizations. Javascript isn’t necessarily slower than Wasm!

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:

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.

Frequent memory allocations can be slow. If you target Wasm with a language like Rust you naturally have to be aware of any allocations and the language makes that relatively easy. On the flip-side if you write poor code that allocates a bunch your code may very well end up slower than equivalent Javascript! But fortunately it’s generally straightforward to improve your allocation strategy.

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.

In Javascript you’re at the mercy of the Javascript engine’s memory layout, which may be quite good! But with Wasm you don’t have to leave it to chance.

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.

What about Javascript ↔ WebAssembly overhead?

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.

A number of people bring up the overhead of calls between Javascript and Wasm as a performance pitfall of Wasm. I don’t think that’s a serious concern in most cases.

Browsers are optimizing that overhead every year and with the right architecture it’s possible to reduce calls between Javascript and Wasm.

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.

Interacting with Javascript from WebAssembly is annoying but not necessarily slow. In the Rust ecosystem there’s a library called 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.

Or if you’re a person who prefers another language over Javascript you may appreciate how Wasm let’s you stick with it.

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.

High-performance audio

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

Multithreaded Wasm isn’t great yet, but fundamentally the highest performance browser code will need to use SharedArrayBuffers. Javascript wasn’t architected with multithreading and shared-memory in mind, but most Wasm languages (like Rust) were.

If you use Rust and multiple webworker threads backed by SharedArrayBuffer memory then inter-thread communication becomes the same as native. Javascript struggles a bit here as it has to serialize and deserialize things into a 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

In Summary

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

Zaplib’s post-mortem