Catching the Web up to speed with WebAssembly

Yordan Yordanov
The Startup
Published in
9 min readJul 22, 2019

Video killed the radio star. Didn’t it?

Photo by Tracy Thomas on Unsplash

Back in the 50s and 60s, television shows become more popular than the ones on the radio. It became the new thing.

There’s a word going around that a new thing in the web development world is here to end JavaScript’s monopoly on client-side programming. We’re going to try and demystify that rumour on a very high level. We’ll start with the natural questions that occur when you meet a new sort-of-an animal, like ‘Whaaaat?’, ‘How?’, and so on. We’ll look into why do we need it and what are its implications. To answer all of these questions, we’ll need to open up some history books and talk a little bit about compilers. But first, let’s see what WebAssembly actually is.

What?

WebAssembly is a binary format that is executable by web browsers that support it and most modern web browsers do. Similarly to any other assembly language, it is essentially set of rules on how to interpret bytes, which roughly consist of some operations and some data. It is a stack based machine, meaning that operations can push and/or pop data on a stack.

Let’s see an example — a very simple program that adds 1 and 1 together and then sends it for output to a magical method called $print . The program is shown in a mnemonic format, so that it could be read by people

1 i32.const 1
2 i32.const 1
3 i32.add
4 call $print

We start by putting a 32-bit constant integer value of 1, and then another one on top of it. The add operation pops 2 items off and instructing the ALU to do an addition. After the computation is done, the result is pushed right back in the stack. Finally the magic method would pop the value out and do some magical stuff with it. Piece of cake.

When dealing with WebAssembly, the web client first decodes these instructions into the machine’s equivalent ones, then executes them as shown above. The decoding stage is really fast, as it’s already very close to a machine language. The execution stage could be pretty performant (more on that later). All of this is done in a sand-boxed environment, meaning that you don’t have to worry about security issues (memory exploits, etc). WebAssembly also has JavaScript bindings, enabling it could talk to each other, which is particularly useful when you want to make changes to the DOM from your program.

Unless you are a hardcore assembly programmer, it could be a really daunting task to write anything more useful than adding 1 and 1, as shown in the example. You’d probably prefer to go for a higher level language.

How?

WebAssembly could be generated by using LLVM toolchains. They are designed to enable and speed up the process of compiling binaries for multiple architectures, doing some pre-optimisations of the source by transforming it into a so-called Intermediate Representation (IR). Essentially, this is an abstract machine code. It’s not comprehensible by the machine yet, but it’s much closer to the machine than it’s original source counterpart, and as such, it can be turned into a architecture specific one much easier. The toolchains you can use today to compile to WebAssembly are Emscripten and PNaCl.

In a traditional scenario, the source code is firstly getting transformed into a IR. This is known as the optimisation stage, which is designed to deduct the low level operations required to fulfil the high level instructions from the source. As we said already, these low level instructions are not quite low enough yet, so in the next stage — compilation, they are getting transformed into many architecture-specific binaries and getting linked to the required static libraries. The result is an executable which can be ran only on that specific architecture

In the case of WebAssembly, the IR will be compiled to the WASM specification into a .wasm file, which you can ship alongside your application. The toolchain will also generate a .js file containing the bindings required to talk to your WASM program. A web browser that received those will proceed as explained in the previous section.

There are some alternatives to this. Turboscript is an TypeScript-like language that compiles to WebAssembly. And there a lot more languages in the pipeline to come.

Why?

So why would we bother with all of this, given that JavaScript has been supported in browsers for decades and provides a nice, easy to use high level paradigm? To answer that, we need to take a look on the overall trend of web applications over the years and whether JavaScript is able to keep up with it.

The story behind…
The Web started with Tim Berners Lee’s first-in-the-world website in the very early 90s. His goal was to create a web of information, where you could click on certain elements in the text, which would take you to other pages (probably with more information on what you’ve just clicked). At this stage, pages were created by writing HTML manually and the contents was all static. People started realising the power of the Web, and gradually the pages were becoming more interactive with the addition of JavaScript, and a bit later all the JS frameworks we know and use today. On the server side things started to change too, from serving static html and JS files to invoking server-side scripts (e.g. via CGI) to create dynamically generated HTML pages. All of this set an important trend — to bring the web applications closer to the native experience, and we’ve been following it ever since.

Legend has it that JavaScript was created in a matter of days by Brendan Eich in the 90s during his time in Netscape. They had just joined forces with Sun Microsystems to take on the MS Internet Explorer. Initially Eich was keen on making the Scheme language work in their browser, but under Sun’s influence and the growing popularity of Java at the time, he ended up creating a Java-like language called Mocha, which, after several renamings became known as Javascript. It was designed to do quick-and-dirty scripts to accompany the HTML, not to be doing any heavy lifting.

It was meant to be a companion for Java, which was also used in the browser for computation heavy things. Developing Java apps was comparatively harder than scripting, they were heavy, required a lot of bandwidth to download and ran in a in-page window with no access to the DOM. JavaScript had the exact opposite intention — to be able to dynamically manipulate the DOM fairly easily.

In the next 10 or so years, JavaScript was getting more and more popular and widely adopted. Java eventually died off on the client side, while technologies like AJAX and frameworks like jQuery pushed JavaScript's capabilities of writing better user interfaces. However its performance remained pretty much the same, until around 2008 performance wars took place.

It was a battle between web browser vendors to provide optimised JavaScript performance. For the first time since the Web’s inception people were able to see improvements in that area. High level frameworks (such as React and thousand others) were created, allowing easy and painless creation of highly responsive elements in the page. It also took JavaScript outside the web browser into some unexpected territories, like server-side programming with Node.js and mobile apps development with React Native. The key of achieving this was to transition JavaScript from a purely interpreted language to a just-in-time compiled one.

The mechanics
The ability to compile critical pieces of code to a more machine native language meant that you could execute it faster. Different JavaScript engines took slightly different approaches on how exactly they do this, but the general idea is the same.

When the engine gets some code, it turns it into byte-code. This byte-code could then be pre-optimised based on some general knowledge that engine has. It would then go ahead and proceed with the execution. The engine monitor would inspect how the execution goes and if it notices that a certain piece of code is executed multiple times in the same way, it would go ahead and optimise it further.

And we would not have a problem, if JavaScript was not a dynamically typed language. If it so happens that this piece of code was now called with a different data type, the Monitor would signal the Executor that it can no longer execute that code and it has to fallback to an earlier version of the code.

It is important to note that monitoring and compiling do take some CPU time. If performed every now and then when appropriate they can speed things up, as their CPU time would be comparatively smaller to the overall execution time. However, if your code finds itself in a limbo between optimising and de-optimising, then you have a real problem, as the opposite effect might occur — execution time may end up being slower even than the interpret-on-the-go scenario.

Also, as mentioned, different JavaScript engines have different implementations on just-in-time compilation, meaning that your software could behave differently on different web browsers. While this is perfectly fine for common cases like dealing with user interfaces, it may not be so perfect for scenarios where you want to provide a fluid experience (e.g. graphics rendering)

And here’s where WebAssembly comes in to the rescue — it provides an already close to the machine native code, where none of this dynamic stuff could happen. Also, as most of the work is done ahead of time, decoding and compiling WASM takes significantly less time than parsing and optimising JavaScript.

So what now?

At this point you might be thinking ‘Great, let’s compile all the JavaScript to WASM and be done with it!’. It would be nice if that worked, but here’s the catch. We should remember that WebAssembly lives in a very low level world — it has no knowledge of how to manage memory or any high level data types (for now at least). There are two approaches on how you might go about this.

In a naive approach, you’d be compiling a whole JavaScript interpreter to WASM, just to mitigate that. So, you’ll end up reinventing the hot water, as the browser already has one, and chances are it will be better than yours.

Another approach you could take is to introduce typing and memory management to your JavaScript source, porting it to TypeScript or something similar. While this may actually be a viable option, you should weight very carefully the engineering work required to do that and the potential gains you may have. More often than not, it may be a better idea to turn your cpu-hungry parts of the code into WebAssembly using any tool you like and just leave the rest of your JavaScript as is.

Is radio still alive?
Yes, it is. Maybe in a different form than it was a while ago, but in 2019, it’s still around. People will still listen to some music on the radio, when appropriate, just as they’ll use JavaScript to build user interfaces for the Web. And just as video and radio are different, JavaScript and WebAssembly are two different tools that can be used together to build things that solve the problem we posed earlier — creating more native-like applications for the Web.

You can think of JavaScript as a Swiss knife, a tool that’s always handy and can get the job done, while WebAssembly can be seen as a surgical tool (e.g. a scalpel) and can be used wherever you need that precision. WebAssembly is nowhere near killing JavaScript and it does not aim to do so. What it aims is to bridge the gap between web applications and the computational power available on most devices today. But yes, it does end JavaScript’s monopoly as it introduces a whole variety of languages that can be used to write for the client side.

--

--