Just for reference I was testing this the other day and compiled some simple C++ to WASM and adding:
std::cout << "some text";
to the code increased the binary size by like 5mb. Turns out std:cout pulls ALL currency-formating, date-formating and i18n code in the c++ standard library into your binary
ansi C printf does not meaningfully increase your WASM binary size
If you want your code to be able to be loaded on-the-fly and fast you need bundling tools, just like JS does. Bundling is a really hard problem, game devs struggle a lot with it as well (although their problem is usually bundling assets, not code itself)
- A custom panic message (so you know which line of code crashed)
- Some monomorphized formatting code to output that message
- The infrastructure to generate a stack trace after a panic
- Logic to free all the allocated objects all the way up the stack
If you compile this 1 line function:
pub fn read_arr(arr: &[usize], i: usize) -> usize {
arr[i] // (equivalent to 'return arr[i];')
}
... You produce 20 hairy lines of assembler: https://rust.godbolt.org/z/dhz34KEvjIn contrast, the equivalent C function is this rust code:
pub fn read_arr_unchecked(arr: &[usize], i: usize) -> usize {
unsafe { *arr.get_unchecked(i) }
}
And predictably, the result is this gem - identical to what the C compiler outputs: example::read_arr_unchecked:
mov rax, qword ptr [rdi + 8*rdx]
ret
But nobody writes rust code like that (for good reason). You can get a lot of the way there by leaning heavily using rust's iterator types and such. But its really difficult to learn what patterns will make the rust compiler lose its mind. There's no feedback on this at compile time, at all.The things I reach for in practice are godbolt and cargo asm[1] - which can show me the actual generated assembler for functions in my codebase. And twiggy[2], which can tell you which functions are the biggest in your compiled binary and point out where monomorphization is expensive.
When I’m developing, I regularly run a script which compiles my code to wasm and tells me how the wasm file size has changed since the last time I compiled it.
Some tips:
Try to avoid array lookups with an index when you can. When looping, use slice iterators and when making custom iterators, wrap the slice iterator rather than storing a usize index yourself.
Be careful of monomorphization. If you’re optimising for size, it can be better to take a dyn Trait rather than making a function generic.
And play around with your wasm API surface area. It takes a lot more code to pass complex objects & strings back and forth to javascript than other types.
But otherwise, good luck! Love the project.
The problem is that it's real easy to just add a bunch of crates to an application, similar to the nodejs/Python approach.
Most people slso don't seem to turn off many parts of the standard library they don't, even for platforms like WASM. Maybe it's useful to have a stack unrolling panic handler during debug but in release you can just abort and save up to megabytes of space.
There's also a lot to be gained by tweaking the compiler optimisers. By default the optimizer is multithreaded, which makes compiles quite a lot faster, but reduce that to a single thread and suddenly a lot of optimizations can happen that wouldn't happen by default.
I wouldn't write code like described here in C, but I imagine Go and C# are better choices here. Maybe even that Java library the name of which I can never remember, or that Kotlin project that compiles Kotlin to Javascript with super easy interaction between frontend and backend.
I love Rust but if you're going to pick a systems programming language for your frontend, just make desktop supplications. Web is a nice fallback but if it's your primary target, there are so many better options out there.