![]() |
|
![]() |
|
More things that I've found helped: - Use wasm-opt from binaryen. That seems to reliably drop wasm size by ~20-30%. ( https://github.com/WebAssembly/binaryen ) - Use brotli compression for serving wasm bundles to the browser, and make sure your web server is setup to use the brotli compressed files. (Its a 1 line change in nginx, for example). Brotli drops the size of wasm bundles by about 3x. Its much better than gzip for wasm. |
![]() |
|
I've been working on a C++ deep RL library (RLtools) and also created some WASM examples (https://rl.tools). I didn't pay any attention to the binary size at all but it turns out it is also just around 200-300kb (including everything, deep learning forward/backward, RL algo and dynamics simulation). Even though it's not prohibitive rn, I'm curious how small it could be and hopefully find some time soon to squeeze it down |
![]() |
|
Strings seem to have a high cost because there's a lot of complexity in rust's format! macros. And the generated code seems to end up with a lot of obscure ways it can panic. I've found just having one stray dbg!() in my rust code can add ~20kb to my wasm bundle size. Look at what a single dbg!(some_u32) generates: https://rust.godbolt.org/z/bex9z8vx7 . Its also calling into a bunch of garbled functions in the standard library - which will bring in a lot more code. I suspect zig's comptime approach might work much better for for this sort of thing, if we want smaller binary sizes. |
![]() |
|
I could be wrong, but I've heard that the term originated in Smalltalk. Smalltalk is also image-based, and tree-shaking was a way to produce a smaller image for deploying.
|
![]() |
|
References: Lucid Inc. used the word internally at least since 1987, mentioning a treeshaker for Lucid CL 3.0 on the VAX. "Lisp Systems in the 1990s", Layer/Richardson, 1991 https://dl.acm.org/doi/10.1145/114669.114674 From above: "In contrast, tree shaking uses the approach of eliminating --shaking out-- what is not needed in a static fashion. Programmers specify the requirements of their application and the unneeded parts of the Lisp system are removed using their detailed knowledge of the application. The disadvantages of tree shaking are that it requires programmer intervention and that a function which is shaken out cannot be easily restored." "Building Common Lisp Applications with Reasonable Performance", Boreczky/Rowe, 1993, https://dl.acm.org/doi/10.1145/1040032.174174 "Lisp: Good News, Bad News, How to Win Big", Gabriel, (not sure from when this version is, the original article is from 1989) https://www.dreamsongs.com/Files/LispGoodNewsBadNews.pdf From Gabriel's essay: > "1.6.3 The Treeshaker > Most Lisp development systems, including Lucid’s, provide all the resources of the Lisp system by default, and this in turn leads to a style of development in which the programmer makes use of whatever tool happens to be most convenient. Because much of the basic Lisp system (or any development system built on top of the basic Lisp system) will generally be unused by a given application, it is very worthwhile to have a tool for excising these unused parts. This tool is called the Treeshaker. > Treeshaker execution occurs in three phases: walking, testing and writing. In the walking phase, the Treeshaker accumulates a set of objects that need to be included in the saved image. After making this set, the treeshaker runs a test of the application to check that all objects which are used in a typical run have been included. The writing phase then generates an executable image which will run the application. > To a first approximation, the walk phase is just a matter of computing the connected component of the Lisp image (treated as a directed graph in the obvious way) generated by the application’s toplevel function. However, because of the way that Lisp objects are generally connected this usually includes almost the entire Lisp image including the unused subsystems. Therefore the treeshaker uses several techniques to find connections between objects that do not actually need to be followed in the walk." > "The name Treeshaker is meant to be evocative of the idea of actually shaking a tree to dislodge dead branches or other trash." On the more funny side, LispWorks has a keyword to the DELIVER function :shake-shake-shake , which invokes the treeshaker during application delivery. https://www.lispworks.com/documentation/lw80/deliv/deliv-key... |
![]() |
|
How so? There is lots of code (tree). Some of the code is not connected to the entry point (trunk). Tree shaking removes the disconnected parts (loose leaves, dead branches). |
![]() |
|
Those are called widowmakers, and shaking the tree rarely frees them. We had an ice storm this year and there are loads of them all over town still. |
![]() |
|
> If your language’s compiler toolchain can manage to produce useful Wasm in a file that is less than a handful of over-the-wire kilobytes, you can win. I agree that tiny binaries will open up new use cases for wasm! And WasmGC definitely helps. As more context, Java and Kotlin can do fairly well there today, around 2-3 K: https://developer.chrome.com/blog/wasmgc https://twitter.com/bashorov/status/1661377260274720770 Though as Andy says, it depends which APIs you use - I am sure there are Java/Kotlin APIs that would pull in large amounts of code, so you do need to be careful there. But these languages are already doing a lot better than C++ and Rust on code size, thanks to WasmGC (no need to bundle several K of memory management code, in particular). |
![]() |
|
If it really originated in the Lisp context (as someone claimed here), it's because it's about as much about eliminating unnecessary data and metadata as it's about executable code.
|
![]() |
|
> Like, what if you only use the shared cache after N different web sites have requested the same module? That would still let websites perform timing attacks to deanonymise people. There's no way to verify that "N different websites" isn't just the same website with N different names. Though, we could promote certain domains as CDNs, exempt from the no-shared-cache rules: so long as we added artificial delay when it "would have" been downloaded, that'd be just as safe. We're already doing this with domains (HSTS preload list), so why not CDNs? Web browser developers seem to labour under the assumption that anyone will use the HTML5 features they've so lovingly hand-crafted. Who wants something as complicated as:
when we have the stunning simplicity of:
Example modified from https://mui.com/material-ui/react-accordion/. Though, in fairness, the developer UX is much better:
Maybe the problem isn't the libraries. Maybe the problem is us.
|
![]() |
|
The problem is the libraries. Browsers are still mostly incapable of delivering usable workable building blocks especially in the realm of UI. https://open-ui.org/ is a good start, but it will be a while before we see major pay offs. Another reason is that the DOM is horrendously bad at building anything UI-related. Laying out static text and images? Sure, barely. Providing actual building blocks for a UI? Emphatically no. And that's the reason why devs keep reinventing controls. Because while details/summary is good, it's extremely limited, does not provide all the needed features, and is impossible to properly extend. |
![]() |
|
The way Emscripten does it, IIRC, doesn't require any special browser support. The toolchain generates glue code in JavaScript to support calls between dynamically linked Wasm modules.
|
![]() |
|
> Wasm makes it thinkable to do DOM programming in languages other than JavaScript Can't help but picking this out for correction - people have been doing it for a long time in compile-to-JS languages - eg ClojureScript, TypeScript, ReasonML and many others. And people have also been compiling native-ish stuff for the web a long time before Wasm through asm.js & emscripten, like C and through that C-based languages such as Python: https://en.wikipedia.org/wiki/Asm.js#Adoption |
![]() |
|
Nuts are typically harvesting by shaking. Fruit is usually picked by hand since it's more delicate and will get bruised if it falls too far. The shaking is quite violent, but it doesn't hurt the tree. |
![]() |
|
It feels like OCaml is well positioned for this kind of flow analysis to perform aggressive tree shaking. With GC support in WASM OCaml should produce tiny binaries.
|
![]() |
|
The big utility of WASM for me, like OP hints at, is bringing things that would be infeasible to port to the web to it, like, say https://pgaskin.net/kepubify/ and other conversion tools (eg ffmpeg-wasm). Much preferable to downloading something or uploading a file to some random person’s server
|
![]() |
|
> I think the core issue is how everything is entangled by design in oo langs. It's not an issue with object orientation. An issue is that languages of that era often relied on reflection for many use cases. People are actually working hard to rid large parts of .NET from these use cases and mark them as safe for eliminating unused code. When there's the possibility of using reflection to call a method, you don't really know what you can safely eliminate. It might look like nothing is calling `Foo.Bar()`, but what if someone has done `Reflection.getClass(someClass).runMethod(someVar)` and those variables have been set to "Foo" and "Bar"? For example, Dart doesn't allow reflection with precompiled apps because it allows them to safely eliminate unused code (https://docs.flutter.dev/resources/faq#does-flutter-come-wit...). Dart is an object oriented language, but it has eschewed runtime code generation and runtime reflection for compile time code generation. .NET is also headed in this direction, but that doesn't happen overnight. However, as others have pointed out, part of the issue will be that non-JS languages will still need to ship implementations of standard library stuff that's included in the JS runtime in the browser (at least the pieces of the standard library you're using post-tree-shaking). |
![]() |
|
I don't think that's a very good goal. Jettisoning the DOM means jettisoning accessibility and being able to leverage everything that the browser gives you out-of-the-box. You have to render to a canvas and build everything from scratch. I think Wasm is great for supplementing a JS app, not replacing it (e.g. using a Wasm module to do some calculations in a Worker). I like to use the right tool for the job, and trying to use something other than JS to build a web app just seems a little janky to me. At one point, there was a Host Bindings proposal that would enable you to do DOM manipulation (it looks like it was archived and moved to the Component Model spec [1]). That would probably be the ideal way to avoid as much JS as possible. However, browser vendors have been heavily optimizing their JS runtimes, and in some cases, Wasm may actually be slower than JS. I've been following Wasm's progress for several years, which has been slow, but steady. Ironically, I think the web is actually the worst place to use it. There's so much cool non-web stuff being done with it and I'm more interested to see where that goes. [1] https://github.com/WebAssembly/component-model?tab=readme-ov... |
![]() |
|
> But can't you use WASM to create GUI's like Photoshop, with no JavaScript or DOM? Yes, you can. If you have the time and the money. To quote Figma: "Pulling this off was really hard; we’ve basically ended up building a browser inside a browser.": https://www.figma.com/blog/building-a-professional-design-to... And that's just for the canvas part. Figma's UI is React. you need a good UI library if you want to do it like native. And native platforms have those. There's nothing of the sort for any of WebGL/Canvas/WebGPU |
1. avoid floats (fixed point arithmetic saved quite a bit of space)
2. avoid hashmaps (originally used hashmaps since it was easy to port JS maps to, have since ported everything to vecs)
3. avoid strings (for awhile there were no strings, but eventually brought it in for display logic)
4. use a small allocator, like talc
5. avoid dependencies. I only use rand & fxhash. I should probably get rid of rand (fxhash only used to hash game state to check for desyncs)
6. avoid generic diversity. I try to keep a small set of instantiated types, for example Vec is there so no need to bring in Box<[i16]> or anything. Getting away from floats/hashmaps helped reduce type diversity
7. design algorithms with size in mind, I have a couple lookup tables where I pack bits https://github.com/serprex/openEtG/blob/2011007dec2616d1a24d... encodes an adrenaline mechanic where multiple attacks give lower attack power creatures more attacks than higher attack power creatures. Care was taken comparing how much decoding logic cost compared to storing unpacked values. AI evaluation uses 6 bit fixed precision because 64 encodes more efficiently than 128 in webassembly
Similarly there's a targeting mechanism with AND/OR & predicates. I used to have an AST like format with each predicate getting an enum & AND/OR being a slice of expressions. Now each expression is 32 bit integers encoding expression in polish notation, AND/OR have 2 bit codes & predicates are 6 bits (polish notation won over reverse polish here because with polish notation I was able to have AND/OR short circuit evaluation)