Are we WASM yet?

WASM was touted as a thing which would be much better than javascript for making applications which run inside a web browser.

Yet it feels to me like it never really “arrived”, and everybody kept using javascript for their stuff.

So what happened? Does it make sense to develop something using WASM instead of JS (e.g. for interactive fiction), or is it still cooking?

WASM was never touted as a complete replacement for Javascript (except perhaps by ignorant blogs trying to cash in on trending topics); instead it was introduced to replace asm.js. WASM is a general purpose and low-level VM format which notably has no direct IO capabilities. Think of Glulx without Glk. WASM modules can import functions from their host environment which is how they can then do useful things, which means that in the browser you need at least a little bit of JS glue. Maybe in time you’ll be able to directly access more browser APIs without needing a layer of glue, but it’s probably not much of a priority because a little bit of JS doesn’t hurt.

WASM is great! Parchment runs on it (since 2020). So it’s not true at all that it hasn’t arrived. But for a lot of uses cases you’ll still be more productive in JS or Typescript.

5 Likes

I recently posted this in a different thread:

You could even split the two parts of this and just standardise on an IF-centred API (e.g. with a set of functionality similar to Glk), and allow people to produce a WASM chunk which interacts with it using whatever WASM-producing tool they like (I’m a big fan of IF-specific languages, but this way someone more familiar with e.g. C++ could write their IF in that and still drop it into an existing interface).

I wrote a blog post about this a few weeks ago.

The first section is relevant to your question.

Web apps in low-level WebAssembly are usually slower than web apps in JavaScript

When writing most non-web software, you can usually write it quickly in a high-level language (with a rich standard library and garbage collection), but you can get better performance (with more developer effort) by writing your code in a lower-level language like C or Rust.

WebAssembly runs code in low-level languages on the web. That seems like an opportunity to get better performance by rewriting JavaScript web apps in Rust, but it doesn’t (usually) work that way.

When you write a JavaScript web app today, the primary thing you’re interacting with is the Document Object Model (DOM). You’re adding/removing/updating elements and their attributes on the page, and responding to events (like click events, keyboard events, etc.) These are all features of the DOM API.

WebAssembly doesn’t have direct access to any of that. WebAssembly has some limited (and fairly slow) facilities to call JavaScript, and to interact with the DOM that way, but WebAssembly can’t modify the DOM or respond to events directly.

And it’s not going to happen any time soon, because of standardization.

Today, there are approximately two core use cases for WASM:

  1. Porting non-JS code to the web, taking a performance hit. If you have some non-JS code that you can’t/won’t rewrite to JS (perhaps because it’s being actively maintained in a non-JS language), WASM makes it possible to run that code on the web. It would run faster if the code were written and maintained in JS, but if you don’t have time for that, WASM is better than nothing. This is what Parchment uses it for, directly using Bocfel and Glulxe (written in C) on the web. But it’s not fast.
  2. High-performance CPU-intensive pure functions, running off of the main UI thread. You want to transcode a 100MB video from MOV to MP4? In that case, you can pass the 100MB movie to a WASM function. It’s expensive to move data in and out of WASM, but if you’re just doing one input and one output, the cost of I/O can be overcome by how fast the WASM code can run by itself.
9 Likes

The use of WASM for IF is a bit more optimistic.

In practice, Dan’s case 2 looks like this: A web app uses both WASM and JS, with JS handling the user interface and WASM handling internal computation. So

  • The user interface receives some user input
  • It converts the input to bytes and hands it to the internal (WASM) module
  • The internal module does a bunch of computation
  • It converts the result to Javascript types and hands back to the UI (JS)
  • The UI updates the DOM in response

This is efficient if it’s happening “per turn” (like once every few seconds) rather than in “real time” (say 60 times per second).

So, okay, this exactly describes how IF games work. And that’s exactly how Parchment uses WASM.

However, this use case isn’t really better than pure JS. As Dan said, it’s great if you have some C code and you don’t want to hand-port it to JS. But, for example, Lectrote current has three Glulx interpreters:

  • Quixe (JS with hand-tooled JIT code)
  • Glulxe (C compiled to WASM)
  • Git (C compiled to WASM)

It’s nice that all three are available; it’s good that we can compare interpreter behavior across the three. But from the player’s point of view, there’s no real advantage to the WASM options.

5 Likes

Example 1: Winchester High (z5)

Example 2: PLDA (z6 with images)

My z-machine interpreter runs both as a local application and in WASM with only a tiny stub piece of code to abstract some differences in libraries.

Having a single code base for both is a huge time saver for me and lets me stay away from the nightmares that are javascript, typescript and node.

1 Like

Yes that’s what i do. There’s a messaging system between the DSL and the GUI that allows each to be interchanged. It’s a bit like GLK but with a message protocol instead of an API. But essentially the same. Above examples are a z backend.

Here’s a strand back-end DSL

1 Like

For my homebrew IF stuff I use ASP.NET Blazor that runs on WASM. Most of the code is C#, but the UI stuff is based on html, css, js. I know that WASM is there, but I didn’t notice yet much of it. Which means: It runs pretty smooth under the surface, and the performance was much better than expected. The combination Blazor and WASM is pretty reliable, and I can recommend it. About other combinations I can’t say much.

IF and runtime together loads with 9 MB at the start, which is not supersmall, but acceptable.

1 Like

I assume most of that 9 is the .NET runtime? My WASM binaries are around 2MB.

1 Like

The game and the .NET runtime, yes.

I suppose you get to decide what is/isn’t “acceptable,” but 9MB is a lot over a cellular network. And I hypothesize that it doesn’t run very well on typical Android phones ($100 or less); at 9MB, your WASM is going to be constantly pushing code out of an Android phone’s extremely limited CPU cache. It might even run out of memory, failing to load entirely.

The industry standard for “good enough” performance is Google’s Lighthouse Testing Tool. Introduction to Lighthouse  |  Chrome for Developers

If you have to download and run a 9MB WASM file before you can do your first “contentful” paint, you’ll never hit Lighthouse’s recommended target of 1.8 seconds on an Android phone over cellular networks. I’d be surprised if it works in less than 20 seconds. (And… don’t you dare refresh that page…)

If you’re committed to writing in C#, I think your mobile users will have a better experience if you use .NET MAUI (which replaced Xamarin). That way, you can compile your C# to native code, bundle it in an app, and distribute the app on the Google Play Store and Apple’s App Store. The app will probably be much smaller than 9MB, and the app can download in the background. And it will likely run faster, too.

If you want to publish to the client-side web, and you want mobile users to have decent performance, I think Blazor can’t be the right answer.

1 Like

You are right about mobile networks. I have done a ton of work to help this. And there’s even more I need to do. Regarding Stefan’s system, i can testify that it does work quite well on mobiles. Although, it can take a long time to start if you’re not on wifi. It’s just that start-up time.

I have found that mobile bandwidth is, more often than not, appalling.

I stream almost everything. And definitely media. Media is the worse. For people doing just text, things are a lot easier. But as soon as you want audio and images it’s, Hello Problems!

Sound is actually the easiest, because you can stream and play at the same time. Of course, you have to be sure not to drop out.

For pictures, I currently have a collection of different sizes server-side. The app measures bandwidth and chooses the “best”. It has to know in advance what’s there because there’s no time to do round-trip guessing with the server.

One of the things I’d like to do, is to replace the version set with a single large server-side mip pyramid, ordered small-to-large.

Downloads would just stream what it can. As soon as there’s enough for something to go on the screen, do it. Then if there’s time continue streaming and improve the picture.

if the player walks off and needs another picture, it can stop refining the image, which will be neat.

1 Like

I guess I just assumed (or hoped) that WASM was going to replace javascript on the web. Feels like a lost opportunity to me.

Thanks, I did read the article.

I think I have been assuming that WASM was something with the capabilities to replace javascript, but it is actually too low level for that. Feels like they had the chance to replace javascript with something better, but blew it. And I can well imagine that if all the major players require agreement, any progress is going to be glacial.

It’s all a bit disappointing really *sigh*

Andrew, to me the answer is simply a bit more complicated. In Blazor/WASM, JS is part of the security concept. The app has full access to everything that can be done in JS, but is limited to the capabilities of the JS sandbox. Sounds like a clever security concept to me.

For many things you do with the browser JS is still needed, but limited to rather short functions. In my code JS communicates with the DOM stuff, and that’s it.

In my app, 90% of the code are type-safe, compiled C# code, that can be fully traced in VS Debugger etc. Only hot reload is rather limited, but the rest of the development experience is great.

I can only talk about the .NET stuff, there are other implementations for other languages and frameworks. I simply get some different from that what I expected, but I’m still happy with the overall experience. So my experience with WASM is very far from disappointment.

2 Likes

If someone wants to have a look on my test app, send me a message and you get a link.

Next week it’s available in the IFComp.

1 Like

My z-machine client for WASM written in Rust is around 5MB, but I haven’t made any effort to shrink it. It’s the entire GUI (I use egui/eframe) so I don’t have to screw around with the DOM or care about rendering differences across browsers; I get the exact same interface as the local app for free.

1 Like

It’s not really a lost opportunity because that’s not what they tried to do. WASM, as it exists, is built to work with JS; it fills in a usage hole that JS is bad at. It’s incremental. Incremental changes to the web are slow but doable.

If you propose replacing JS, you’re planning something that can do everything JS can but differently. This is a hard sell because JS can do all of that already.

7 Likes

+1 Zarf. A few folks elsewhere have responded to my article saying, “well, they should have tried to replace JS, even if they didn’t!” but the whole point of the article is that doing it would be (would have been) unthinkably difficult.

And the performance gains from replacing JS for DOM access are quite low. It mostly has to do with disabling GC and enabling multithreading, both very hazardous features that are almost as easy to screw up as they are to improve performance. Most web apps wouldn’t benefit from either of these, especially since the UI has to be single threaded, and since you can use multithreading in WASM today.

The other hoped-for benefit would be to run other general-purpose languages on the web, like Python, C#, etc. but all of the other general-purpose languages want to drag around their own standard library in compiled executables, which is a major performance loss. Stefan’s 9MB Blazor app is fairly typical in that regard. Blazor brings its own copy of ICU, for starters. C# strings aren’t quite the same as DOM strings. C# HTTP requests aren’t the same as DOM HTTP requests, which means that Blazor apps include their own reimplementation of an HTTP library, instead of using the one the browser provided. C# files aren’t the same as DOM files. And on and on and on.

If you just find JS “ugly,” and you’re willing to run a build step before deploying to the web (which WASM necessarily would have required), you can already treat JS as a transpilation target today and improve your developer experience that way. You can even provide debug symbols (“source maps,” in transpilation terms) so that you can use the browser’s debugging tools to do stepwise debugging in your preferred language.

IMO, any fan of Blazor should really give TypeScript a serious look, a type-safe language from Microsoft that transpiles to JavaScript. TS has a lot in common with C#; they both have Anders Hejlsberg’s sense of taste, in a good way. TypeScript is now one of the top ten most popular languages in the world. The most recent Stack Overflow survey lists TypeScript as more popular than Java, C#, or C++. TS’s debugging support in browsers is better than Blazor’s, including hot reload, when you use Vite.

5 Likes