Published: Jul 18, 2024|17 min read17 minutes read
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.
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).
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.
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.
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 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).
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):
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.
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.
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.
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.
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.
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.
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.
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.
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.