GitHub icon

WebAssembly, the answer for the Modern Web Performance

This blog was originally published in the Toyota Connected Engineering blogs.

Image Credit: https://github.com/carlosbaraza/web-assembly-logo/tree/master/dist/logo

The Web has evolved so much since its inception, but a few things didn’t change much — JavaScript. In a rapidly changing Web landscape, JavaScript has been the only language that ruled the Browser ecosystem for over a quarter-century now. All this speaks for the greatness of JavaScript!

In recent years, we are pushing the limits of the Web unlike ever before, inviting unconventional web applications built based on Augmented reality (AR), Machine Learning (ML), games demanding intensive graphics, etc. to the web browser. However, JavaScript, the language of the web browser is not meant for building performance-sensitive applications. This is where WebAssembly abbreviated WASM steps into the game, filling the performance void in the web browser.

WebAssembly has been around there for some time now and it is starting to get serious adoption of late. WebAssembly has found its way beyond web browsers, but we will be talking about WebAssembly in the scope of the web browser to keep the article focused.

Understanding WebAssembly

WebAssembly is defined in the official site as

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. WebAssembly is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

We will break down the official definition of WebAssembly as we proceed further, but before we delve deeper, let us define it with a simpler language — WebAssembly is a technology that enables you to run code written in programming languages other than JavaScript in the web browser. Keep in mind this is not an official definition, but it fits the bill in the context of the web browser. Now we have a layman-level understanding of what WebAssembly is. Let’s look at the compilation process and the virtual machine to understand more.

Compilation Process

The idea behind WebAssembly is to take a language like C and run it in the web browser. The C compiler would take the source code and produce machine instructions for the target CPU architecture. But this traditional compilation process is not going to work for the web. Web browsers run on a plethora of platforms that you may not even have come across. We don’t know beforehand the underlying CPU architecture where our code is going to run.

Fortunately, this is not an unprecedented problem in Computer Science. If we look back at the history of Computer Science, it tells us the concept of Abstraction has proved to be one of the most powerful mechanisms to reduce platform dependency. WebAssembly does the exact thing — it introduces a layer of abstraction, which allows us to run the code in the browser, independent of the underlying platform.

WebAssembly compilation process

As seen in the above diagram, we compile the high-level source code written in languages like C, C++, and Rust into intermediate WebAssembly bytecode. Then the WebAssembly bytecode or binary will be shipped along with other website assets like HTML, CSS, and JavaScript files upon loading the web page in the browser. The newly added capability (WebAssembly runtime) in the web browser will take care of translating the WebAssembly bytecode into machine instructions that the underlying processor speaks.

To run WebAssembly code in the browser, the browser engine will have to implement the WebAssembly specification like how ECMAScript specification is implemented to run JavaScript code. The following is a super simplified diagram of the browser engine focusing only on the runtime components.

JavaScript and WebAssembly Runtime

Each browser has its own browser engine. Hence it is the independent browser team effort to support WebAssembly. It is wonderful that all the major browsers such as Google Chrome, Mozilla Firefox, Safari, and Microsoft Edge have implemented WebAssembly specifications in their browser engine (including the mobile version). At the time of writing this blog, the browsers that support WebAssembly are shown in the image below.

Okay, enough theory. To get a feel of the WebAssembly bytecode, we are going to use the WebAssembly explorer site to compile a C code to WebAssembly.

The above code compiled to WebAssembly would look like the bytecode shown below.

If you haven’t seen assembly instructions before, this is how it looks :) Definitely not pretty!

Virtual Machine

The core of WebAssembly is Virtual Machine (VM). Based on the way the instructions are executed, the virtual machines can be broadly classified into two variants — register-based and stack-based virtual machines. WebAssembly is a stack-based virtual machine. Stack-based VM executes the instructions using primitive stack operations — push and pop. As a result of the simple execution approach, the stack-based instructions are on an average 25% smaller than the equivalent register-based VM instructions. But the space advantage comes at the cost of performance. Yes, it is a space-time tradeoff!

The Virtual Machine not just makes WebAssembly platform-independent, but also language-independent. Furthermore, the WebAssembly runtime does not have an automatic garbage collector. As a result, languages that do not need garbage collector like C, C++, and Rust got WebAssembly support earlier than other languages that rely on the garbage collector.

In the scope of Virtual Machines in the browser, Java Applets could be an interesting comparison. Java Applets were primarily used to add interactive features to web applications, and they did a pretty good job given the maturity of the Web at that time. However, there was one serious issue with Java Applets — the Java bytecode runs on the Java Virtual Machine (JVM) external to the browser’s process memory. This setup prevented Java Applet blend well with the user experience of the web application.

Operationally, users must maintain two software — browser and JVM to run Applets. This was painful as the users must keep the JVM version compatible with Java Applets running in the browser. Parallelly, JavaScript engines were getting a series of optimizations like Just-In-Time (JIT) compilation and access to hardware-accelerated GPU which closed the performance gap between Java Applets and JavaScript. As the disadvantages of Java Applets outweighed the advantages, browsers deprecated the Java Applet support. Now, Java Applet is part of Web history.

Thanks to the browsers for baking the WebAssembly runtime as the native component in the browser engine.

When to use WebAssembly?

If we understand the areas where WebAssembly shines over JavaScript, we can understand when to use it. You would often find people comparing WebAssembly and JavaScript, including myself in this blog. This may allow you to overlook the fundamental traits of WebAssembly and JavaScript. They both are two different things altogether — WebAssembly is a compilation target, whereas JavaScript is a programming language. Despite their contrasting traits, they both have a role to play in modern web applications. In the following sections, we will understand the role of WebAssembly by focusing on the key dimensions of any performant web application — performance and load time.

Performance

The execution speed is a crucial parameter when we compare JavaScript and WebAssembly. JavaScript is a garbage-collected language. This inherent nature of JavaScript bites us when our web application must perform CPU-intensive tasks like audio/video streaming, image rendering, media editing, etc. When we closely look at the mentioned use cases, these are operations proven to be handled effectively by low-level languages such as C, C++, and Rust.

Image Credit: https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/

In the execution flow of JavaScript, apart from executing the actual instructions, it involves parsing and tokenizing the source code into AST (Abstract Syntax Tree), compiling, optimizing, and garbage collecting. That is a lot of work to be done at runtime! The non-deterministic garbage collection in JavaScript makes the situation worse in performance-sensitive tasks.

Whereas in WebAssembly, most of the time-consuming execution steps are performed ahead of time at compile time as opposed to what JavaScript does. The decoding and compiling steps in WebAssembly is a lot cheaper because it is already in a language form that is close to what a processor understands. It is evident from the efficient execution pattern, WebAssembly is the answer for the application that craves performance.

If you are thinking compiling JavaScript to WebAssembly would take the performance gap out of the equation, that is not the case. The current state of WebAssembly runtime only worsens the situation. To compile and run JavaScript as a WebAssembly module, we must compile the whole JavaScript Virtual Machine along with the application code. This takes a huge hit on both the performance and size of the WebAssembly module. This situation may change when the WebAssembly runtime includes garbage collection for languages that rely on it.

Load Time

The load time of the web page correlates with a good user experience. One factor that influences the load time is the size of the web page assets. The smaller the size of the assets, the quicker the browser downloads and renders them. The size of the WebAssembly module is many times smaller than the equivalent minified JavaScript implementation.

On top of a smaller WebAssembly module, the streaming compilation feature is an absolute game-changer. Instead of waiting for the whole WebAssembly file to be downloaded, streaming compilation allows the WebAssembly runtime to compile the WebAssembly file in chunks as it downloads them over the network, which reduces the load time multifold.

Image Credit: https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/

WebAssembly and JavaScript together

It is easy to overthink that WebAssembly will be replacing JavaScript given the capabilities of WebAssembly. But JavaScript isn’t going anywhere. Indeed, WebAssembly complements JavaScript and empowers developers to optimize and run CPU and memory-sensitive applications in the web browser. However, WebAssembly kills the monopoly of JavaScript in the browser space for the good, which opens a whole new set of opportunities.

In all fairness, WebAssembly cannot do everything JavaScript does. WebAssembly module runs in a sandbox environment, which does not have access to Document Object Model (DOM), I/O, etc. Though this may sound restrictive, the strict isolation eliminates the attack surface the malicious code could exploit. If you intend to perform any I/O operations like a file or network I/O, you must achieve it through JavaScript. This reassures that it is not either WebAssembly or JavaScript, but both have a role to play their strengths in the Web.

Communication between WebAssembly and JavaScript

The communication between JavaScript and WebAssembly can be an independent article on its own. However, to give you a fair idea, let’s talk a little about a key element of the communication between JavaScript and WebAssembly, which is data types.

WebAssembly has only 4 data types as follows.

  • i32: 32-bit integer
  • i64: 64-bit integer
  • f32: 32-bit floating-point
  • f64: 64-bit floating-point

On the other hand, JavaScript has rich data types like any other high-level language. The disparity between JavaScript and WebAssembly data types leads to questions like, “How can a JavaScript String type be represented in WebAssembly?”. Any JavaScript data type will be translated down to integers and floating points. Likewise, when the call is made into JavaScript from WebAssembly, the WebAssembly data types will be translated into JavaScript types. Apart from JavaScript APIs, browsers provide Web APIs like console.log(), Window.alert(). There is no direct way to call the Web APIs from WebAssembly, it has to go through JavaScript. As the WebAssembly team is working on improving this inefficiency, this situation is likely to change in the future.

The next logical question would be, “How is the data being passed between JavaScript VM and WebAssembly VM that makes the communication possible?”. They communicate by sharing memory — both the VMs have access to a shared linear array of memory. The image below illustrates the scenario, where the WebAssembly module sends an ASCII encoded Hello message to the JavaScript module by writing it in the shared linear memory. In addition to the message, the WebAssembly module passes the starting address (offset) and the length of the message to the JavaScript module to read the data from the linear memory.

Image Credit: https://hacks.mozilla.org/2017/07/memory-in-webassembly-and-why-its-safer-than-you-think/

Thankfully, developers need not write low-level code to read and write from the linear memory and convert the data types between JavaScript and WebAssembly. Tools emscripten and wasm-bindgen will generate JavaScript glue code facilitating the communication between JavaScript and WebAssembly module.

JavaScript and Rust-generated WebAssembly

In this section, let us take a code example to understand how data is transferred between JavaScript and WebAssembly. Rust is the language we will be using to build the WebAssembly module.

There are a few Rust elements we need to understand in the context of WebAssembly.

  1. wasm_bindgen is a Rust library (crate) and also a Command Line Interface (CLI) tool that facilitates the high-level interaction between JavaScript and Rust-generated WebAssembly module. For example, Rust code can use native &str type instead of a slice of u32 to handle JavaScript String type. wasm_bindgen crate allows making calls to all the JavaScript and Web APIs from the Rust code.
  2. When a public function or extern block is annotated with the attribute #[wasm_bindgen], it tells the Rust WebAssembly compiler to generate the bindings and JavaScript glue code for the decorated function or a block.
  3. There are a couple of meanings to the extern keyword in Rust. In the following context, the extern keyword is used to declare Foreign Function Interface (FFI) such that the Rust code can call those foreign functions.

The following Rust code imports the Web API Window.alert() and exports the Rust function greet to JavaScript.

Note that greet and alert functions accept native Rust type ( &str) as a parameter and not a pointer to the linear memory. Thanks to wasm-bindgen for making this high-level interaction possible.

wasm-pack is a popular tool to build WebAssembly packages written in Rust. wasm-pack internally uses wasm-bindgen to wrap out Rust-generated WebAssembly module by JavaScript wrappers. On building the above Rust code using wasm-pack, it generates the JavaScript wrappers to the WebAssembly module as shown in the image below.

JavaScript glue code generated by wasm-pack

The generated glue code allows the JavaScript application code to call the WebAssembly functions like calling any other JavaScript function. The following JavaScript code uses the generated glue code to call the greet function in the WebAssembly module and passes the argument World! of type string.

Yay, we have successfully connected all the strings and the output is shown below.

Output

Let us go through the steps to understand what happens under the hood to achieve the above output.

  1. The string World! is passed to the JavaScript glue code from the JavaScript application code.
  2. The JavaScript glue code converts the string into numbers (ASCII code) and writes in the linear memory.
  3. JavaScript glue code also passes the length and pointer to the start of the message to WebAssembly.
  4. WebAssembly reads the message (World! in ASCII code) from the linear memory and appends it to the ASCII equivalent of the string Hello, .
  5. The Web API call Window.alert() from WebAssembly has to go through JavaScript. Hence, the ASCII values of the string Hello, World! are written to the linear memory and the starting address and length are passed to the JavaScript glue code.
  6. JavaScript reads the message from the linear memory and decodes the ASCII values into JavaScript String.
  7. JavaScript makes the Web API call Window.alert() and passes the argument Hello, World!.

It is amazing to witness how well all the underlying complexities are abstracted away from the application code.

Some inspirational works using WebAssembly

Resources

There are tons of resources on the Internet that talk about WebAssembly in great breadth and depth. Some of the resources that I found useful are listed below.

Show Comments