Feasibility of wasm as a replacement for javascript

I have a love/hate relationship with javascript. I love what people have done with it, but whenever I have to use it myself I get the strong urge to go out into the woods and reject technology completely.

I've been looking at the web-sys crate, and after a quick surface scan it seems to have all the pieces we need to convert our modest js scripts to Rust/wasm. However, I'm wondering if there are hidden dangers lurking under the hood of it. I recall reading a while back that there are some situations where one can accidentally leak memory in the Rust code (due to the js/wasm glue), and I'm not to keen on doing that (we have a web interface that is used to monitor some embedded devices, and these are potentially very long running scripts).

I guess I'm asking if web-sys is full of thorny traps -- specifically wanting to hear from people who Rewrote It In Rust/wasm [from js].

(Our scripts very pretty basic -- the most advanced things we do is use SSE to get the server to send status notification changes, custom form submission, simple DOM manipulation. Nothing out of the ordinary).

The first thing you need to know is that WebAssembly is not a replacement for JavaScript. As they are currently implemented, the only way your code can interact with a browser is by going through the browser's JavaScript API.

What the web-sys crate does is provide glue code that will call the corresponding JavaScript for you.

There aren't any thorny traps, per-se.

You just need to keep in mind that web-sys is trying to make JavaScript APIs (complete with JavaScript's loosely typed semantics) accessible from a strongly typed language like Rust. That means calling functions from the web-sys crate might feel awkward or clunky because, for example, you need to handle the fact that most JavaScript functions can accept/return just about anything, including undefined or null.

This isn't typically an issue once your project gets larger (e.g. above 2kloc) because you'll be creating your own abstractions on top of whatever the browser gives you.

However, for something which is very closely coupled to the browser APIs (e.g. because you do loads of DOM manipulation), it'll be hard to abstract everything away and awkwardness from the implementation details will leak out.

I suspect this is one of those situations where someone wrote some bad code or there might have been a bug in wasm-bindgen's glue code, and the whole situation got blown out of proportion.

It might have been that they were using the wee_alloc crate as an allocator because it adds a lot less machine code to your binary than the default (dlmalloc, I think). However, because it's optimised for code size and making a small number of allocations on startup, not performance, the allocator can be prone to heap fragmentation, which can eventually lead to an OOM. This happened to me on a previous project and it was like a 2 line fix.


To get back to your post's original question... it depends.

For a lot of utilities and core business logic it's quite feasible to use WebAssembly and take advantage of Rust's strong points (strong typing, fine-grained control over memory, easy bit/byte-wise manipulation, Option, etc.). Then you can use JavaScript to glue the Rust core to the actual web page.

However, that only makes sense once you've reached a certain scale. Rewriting a 500 line JS script in Rust is probably more effort than it's worth because half the code will be JavaScript glue, but the story might be different for a 5-10kloc program.

7 Likes

For the majority of use-cases, I would probably go for typescript over wasm.

2 Likes

Regarding the memory leaks, it's a risk when using the web-sys api directly. Things like:

        use wasm_bindgen::prelude::*;
        use wasm_bindgen::JsCast;

        let cb = Closure::wrap(Box::new(move || {
            // some action
        }) as Box<dyn FnMut()>);
        some_html_elem
            .add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())
            .unwrap_throw();
        cb.forget();

(Check the paint example)
You can otherwise store the cb somewhere (in some html element wrapper, i.e. along with the element) and free it at some point. You can check how some wasm frontend crates deal with it.

Another (simpler) option is when generating the js glue using wasm-bindgen (cli), to pass the --weak-refs flag, see here. This leverages the runtime's garbage collector, a downside is that it might not be supported by older browsers.

There is one trap which is large enough as to make all efforts to abandon JavaScript pointless.

Only JavaScript have synchronous access to DOM.

This means that wasm is great for many things except for replacement for JavaScript in browser.

If you are in more controlled environment where no one touches your DOM behind your back then there are some chance of making it work.

On the open web? Forget about it.

Everything would work on your system and/or CI system, but would break in hundred of really horrible ways in the wild because of adblocks and other extensions people use.

Just not worth it.

Try setting up a callback handler in Rust.

I have only found two ways: std::mem::forget(... on the handler...), or storing the handler in some type of circular Rc<RefCell<Option<...>>> monstrity.

At this point, I've gone with the std::mem::forget route, but (1) I only call them a constant number of time, and (2) only in top level functions, so that a quick lexical scan ensures that I only call them a constant number of times.