WebAssembly (Wasm) is a binary instruction format that offers a compact and efficient way for executing code across diverse environments, including the web.
Previously, Scala couldn’t directly compile to Wasm, but now Scala.js will support Wasm as its new linker backend, thanks to the collaborative efforts of the ScalaCenter and VirtusLab. 🎉🎉
Initial implementation of the WebAssembly backend. by sjrd · Pull Request #4988 · scala-js/scala-js
This development may raise a question: why does Scala aim to compile to Wasm, and how is this achieved?
This article explains
How Scala.js supports Wasm
The current status of Wasm support in Scala.js
Benchmark
What does it mean to us?
Wasm Garbage Collection
Why Scala.js, not ScalaNative
How Scala.js supports Wasm
Scala.js is a Scala dialect that targets the JavaScript platform. It compiles the Scala code into Scala.js Intermediate Representation (SJSIR), and subsequently links the SJSIR to JavaScript.
The new Wasm linker backend processes SJSIR files and generates a Wasm binary accompanied by a small JavaScript helper file.
Users can enable the Wasm backend by setting withExperimentalUseWebAssembly(true)
in the Scala.js linker settings. (Additional configuration will be necessary; details should be documented in Scala.js documentation upon release 📝).
The current status of Wasm support in Scala.js
Currently, executing a Wasm binary generated by Scala.js requires JavaScript engines such as those in browsers, Node.js. Standalone Wasm runtimes like wasmtime and WasmEdge are not yet supported, but we plan to extend compatibility to these non-JavaScript runtimes in the future.
Our implementation utilizes WebAssembly Garbage Collection (WasmGC) and Exception Handling extensions, so the Wasm binary runs on runtimes that support these features.
For instance, V8 and SpiderMonkey had supported WasmGC, with Chrome 119 and Firefox 120 enabling WasmGC by default. JavaScriptCore is also actively working on WasmGC support.
Among the non-JavaScript runtimes, WasmEdge 0.14 supports WasmGC, and wasmtime is in the process of adding support.
Benchmarking
Run-Time Performance Analysis 📊
Given Wasm's design emphasis on efficiency and near-native performance, it is essential to demonstrate that the generated WebAssembly code is competitive with the JavaScript produced by Scala.js.
We ran the scalajs-benchmarks on an Apple M2, MacOS Sonoma 14.5, Node.js v22.3.0, and V8 12.4.254.20-node.13.
(Note that the Scala.js compiler for this benchmark contains several work-in-progress optimizations for Scala.js Wasm backend done by sjrd 🎉)
The graph below presents a comparative benchmark of Wasm vs JS (both compiled from Scala.js). The y-axis represents the relative performance of Wasm to JS.
A value of 1 indicates parity with JS, while values above 1 demonstrate how many times faster Wasm performs. Conversely, values below 1 indicate that JS outperforms Wasm.
Wasm demonstrates exceptional run-time performance for tasks with minimal or no JS-interop, such as SHA512
, where it performs up to 5.8 times faster than JS. For most tasks, Wasm shows competitive performance, often outperforming JS by 1.5 times.
Benchmarks with heavy JS-interop, such as deltaBlue
, sudoku
, kmeans
, and permute
, currently show slower performance in Wasm compared to JS, indicating areas for further optimization (or JS is better for those tasks).
For more detailed information, please visit here.
Code Size Analysis ⚖️
One of the significant advantages of Wasm is its compact binary format, which typically results in smaller code sizes compared to JS. Below is a comparison of the code sizes for Scala.js test suites (fullLinkJS
):
JS (with Google Closure Compiler): 4.1M
JS (without GCC): 14.5M
Wasm: 6.4M
Benefits of Wasm Compilation
Smaller Code Size: Wasm typically generates more compact code compared to JavaScript. This contributes to faster download and decoding times.
Fast Execution: Wasm compilation leads to faster execution times, particularly for tasks with minimal or no JS-interop.
Optimization Potential: Optimizations including the Binaryen optimizer (which isn’t yet available) can further improve Wasm performance and reduce code size, making it an even more compelling choice for web applications.
While these benefits are substantial, they are not the entirety of Wasm's potential. The advantages of Wasm extend far beyond these points.
What does it mean to us (in the future)?
Currently, Scala.js's support for Wasm is limited to usage within JavaScript environments. However, future developments may enable its use outside browsers, cross-language communication, and implementing functionalities through new Wasm proposals.
Non-Browser Embeddings
While Wasm was initially designed for browser environments, its fast startup times, sandboxing, and portability make it valuable in various other contexts:
Serverless Environments: Wasm's rapid startup times are well-suited for serverless computing, where minimizing cold start latency is crucial for responsive scaling (e.g., Fermyon Spin, Fastly Edge).
Containers: The combination of container technology and Wasm is highly effective due to Wasm’s portability and sandboxing capabilities. Wasm provides a secure sandbox without compromising startup and run-time performance compared to traditional Linux containers: WebAssembly vs Linux Container.
Plugins and Extensions: Wasm's sandboxing capabilities allow for the safe execution of user-written, untrusted code, making it an ideal choice for implementing plugin systems or extensions in applications (e.g., Envoy, VSCode, Shopify Functions).
Wasm Component Model
Currently, there are no standard methods for direct inter-module communication, and interface types are limited to basic numerics. When modules export interfaces using higher-level types such as strings, records, and enums, data must be laid out in linear memory for proper interpretation by the module, which undermines Wasm's portability.
The Wasm Component Model addresses these issues by standardizing communication between Wasm components using high-level types. This model enables direct interaction between components built in different programming languages, enhancing interoperability and preserving portability.
Wasm Proposals
In addition to the Component Model proposal, various other Wasm proposals aim to extend Wasm’s capabilities. One particularly interesting proposal is the stack-switching proposal, which will serve as a foundation for efficient implementation of lightweight threads in Scala.js.
Why Wasm Garbage Collection (WasmGC)
Back in 2019, there was already interest in supporting Wasm within the Scala.js ecosystem.
However, a critical challenge hindered the adoption of Wasm: the absence of native garbage collection (GC) support. To compile Scala to Wasm without native Wasm GC, we primarily had two options, each with substantial drawbacks::
Compiling the JVM to Wasm
One approach was to compile the JVM itself to Wasm. This method, exemplified by CheerpJ, is effective for bringing legacy Java applications to the browser. However, it results in very large module sizes because the entire virtual machine must be included in the Wasm module. This counteracts one of the main advantages of Wasm: its fast startup and execution times.
Embedding a Custom Garbage Collector
The alternative was to embed a custom garbage collector within the Wasm module. While it’s feasible, this approach introduces performance and interoperability issues .
It requires installing a shadow stack within Wasm linear memory to collect GC references on the stack, as Wasm prevents programs from inspecting their own stack. Additionally, it creates a GC-related interoperability problem between Wasm and JavaScript, known as the cycle-collection problem.
For more details, refer to the V8’s blog post explaining the overview of WasmGC: “A new way to bring garbage-collected programming languages efficiently to WebAssembly · V8”
WasmGC to the rescue
These challenges can be addressed by a Wasm native garbage collection proposal known as WasmGC. This proposal provides GC-managed data structures and instructions built into the Wasm specification, allowing allocated heap values to be managed by the Wasm runtime's garbage collector.
;; struct / array definition
(type $point (struct (field $x f64) (field $y f64)))
(type $vector (array (mut f64)))
;; allocate
(struct.new $point (f64.const 1) (f64.const 2))
(array.new $vector (f64.const 1) (f64.const 3))
WasmGC enables garbage-collected languages like Kotlin, Java, OCaml, and Dart to be directly compiled to Wasm.
Five years ago, WasmGC was in its very early stages, and none of the Wasm runtimes supported it. By 2024, WasmGC had gained support from various runtimes, making it the right time to support Wasm in Scala with WasmGC.
How about ScalaNative?
While we have discussed the rationale and methodology behind supporting Wasm with Scala.js and WasmGC, it is important to consider another approach: Scala Native.
Scala Native, which compiles Scala code into a native binary via LLVM IR, initially appeared to be a promising path for Wasm support in Scala. By leveraging LLVM, Emscripten, and WASI-SDK, it could target Wasm and even enable Scala Native libraries to function seamlessly in standalone Wasm runtimes, thanks to wasi-libc. In fact, we conducted an experiment to explore Wasm support with Scala Native.
However, a significant limitation emerged: LLVM does not support WasmGC. This constraint necessitated embedding our own garbage collector, which, as previously discussed, leads to performance degradation and interoperability issues.
Future
The initial implementation of Wasm support in Scala.js is not yet fully optimized. Our Scala experts plan to further enhance the Wasm backend by enabling the Scala.js optimizer for Wasm, leveraging the js-string-builtins proposal, and implementing additional optimizations.
Moreover, we aim to extend Scala-Wasm's applicability to non-browser runtimes. This will empower developers to create serverless applications or extensions using Scala in Wasm environments beyond the browser.
Summary
In this post, we've explored the integration of Scala with Wasm:
Scala.js supports Wasm as a new linker backend: This integration allows Scala.js to compile to Wasm, opening new possibilities for Scala applications.
Current Wasm support is limited to JavaScript environments: The benefits include smaller code size, faster startup times, and potential performance improvements for compute-intensive tasks.
WasmGC is crucial for effective Wasm support in Scala: Native garbage collection in Wasm is essential to avoid the performance and interoperability issues associated with embedding custom garbage collectors.
Scala.js was chosen over Scala Native: This decision was driven by LLVM's lack of WasmGC support, which would have necessitated embedding a custom garbage collector, leading to the aforementioned drawbacks.
While our Wasm support is still in its early stages, this integration positions Scala at the forefront of the evolving Wasm ecosystem. We are excited about the future possibilities and are committed to further optimizing and expanding Scala's capabilities in the Wasm landscape.
thanks for the detailed explanation. i've been wanting to try wasm using scala for a long time and you guys just made it possible. what an heroic effort!!
i have a couple of questions though:
1. the js generated after fullLinkJS is actually smaller than wasm by quite a big margin. shouldn't wasm code be smaller than js?
2. when can i try it?
Out of curiosity: How does Scala Native handle garbage collection? And why wasn’t it an option to do it the same way for scala.js compiled to WASM?