An easy way to boost your client-side Javascript using WebAssembly(Wasm)

Timothy McCallum
Wasm
Published in
10 min readNov 20, 2020

It is well known that “WebAssembly is not here to kill JavaScript” [1]. We use Javascript as an integral part of the Web, and it is here to stay. In saying that, Wasm is growing fast, in terms of both popularity and also usage. Why? Because it offers key features such as safety and performance. In reality Javascript and Wasm mesh together nicely to benefit users of the Web.

Chris Williams, Public domain, via Wikimedia Commons
Carlos Baraza, CC0, via Wikimedia Commons

Let’s get straight to the point. In cases where Web applications require specialist tasks to be performed, can those tasks be farmed out to a high-performance stack-based WebAssembly(Wasm) Virtual Machine(VM) via the Web?

Specifically, because we are talking about Web enabled components, can Javascript code make a call to a Wasm Function as a Service(FaaS) in a way which would actually benefit the client side Javascript’s goal, and in doing so benefit the entire Web application as a whole. If so, then we can safely say that we are benefiting the user[s] of that Web application.

Let’s quickly cover off what a FaaS is and then look at some use cases where Wasm via FaaS could support client-side Javascript.

What does Function as a Service (FaaS) mean?

The term Function as a Service (FaaS) refers to executing the logic of pre-existing functions via secure HTTP requests, over the Web.

Simply put, you no longer need to run your own servers. Instead, you can use existing functions that other developers have written, or you can create and deploy your own custom server-side functionality as a FaaS.

SecondState’s FaaS Website provides many great examples, demonstrations and tutorials which will help you to take full advantage of FaaS technology. Also this article dives quite deeply into Web programming using FaaS technology. After getting familiar with FaaS, you will find yourself in the territory of being able to create zero-infrastructure applications; which is very exciting!!!

What does zero-infrastructure mean?

In our previous article, “Implementing zero-infrastructure web content in less than 5 minutes”, we showed you how to utilise FaaS technology to create powerful Web applications.

These Web applications can include functionality such as AI inference (image detection), image manipulation (watermarking images, flipping images) and much much more.

In a zero-infrastructure implementation, your frontend (client-side) application does not even need to he hosted on a server. It can consist of (as little as) a single HTML page with some Javascript. This page can even be accessed locally on your user’s devices using thefile:// URI scheme.

Any server-side logic, that your application requires, will execute seamlessly without you having to build or maintain any of your own server-side infrastructure. No more firewall configuration, network security issues, storage concerns, load balancing configuration, OS patching/updating. Oh! and no more electricity bills.

I hope all of this content inspires you to join this ecosystem and perhaps even share your own custom functions with others.

With those brief explanations sorted, let’s now take a dive into learning how FaaS can extend client-side scripting languages like Javascript.

Limitations of client-side scripting languages

Client side languages like Javascript do not have certain built-in functionality. For example, client-side Javascript does not perform particularly well, when processing binary data, and has no built-in functionality to do so.

Being able to process binary data is super important. If we can process binary data then we can process image data, binary files, binary network protocols and so forth.

As web applications become more and more powerful, adding features such as audio and video manipulation to these client-side Javascript applications would be ideal [2]. But how do we quickly and easily manipulate raw binary data in Javascript. Generally speaking we, as developers, have to write our own siloed custom code. As a specific example, if I want to see if a sequence of bytes (in an ArrayBuffer) is present or absent in another ArrayBuffer, I will need to port over some C code or design and write something from scratch.

Client-side Javascript does not have a built-in “sequence of bytes” comparisons feature.

We are going to see if this is something we can solve with FaaS. Reason being, it is not ideal that every developer is required to re-implement byte manipulation code from scratch over and over again. In fact, software design principles like DRY (Don’t Repeat Yourself) encourage code to be written once and reused often. This is where FaaS can help.

Excitingly, there is also a slight chance that a remote FaaS implementation of this task may be faster than a client-side Javascript implementation. Let’s create the solution and find out!

Data for the FaaS

Creating ArrayBuffers for testing

If you were building a bytes comparison FaaS call into your App, then the data which your client-side application were processing (searching/matching) would most likely come from some part of your application i.e. images, files etc. However, for simplicity sake, we are just going to create some arbitrary data in this article (so we can get on with explaining our design and implementation).

The following ArrayBuffers are one million bytes, in size.

const buffer_1 = new ArrayBuffer(1000000);
const buffer_2 = new ArrayBuffer(1000000);

We can also go ahead and confirm the length of the ArrayBuffers; how many bytes are present in buffer_1 and buffer_2

buffer_1.byteLength;
buffer_2.byteLength;
//1000000
//1000000

Remember, these are just ArrayBuffers, not views; as the following code confirms for us.

ArrayBuffer.isView(buffer_1);
ArrayBuffer.isView(buffer_2);
//false
//false

The ArrayBuffer object is used to represent a generic, fixed-length raw binary data buffer. It is an array of bytes, often referred to in other languages as a byte array[3].

Javascript typed arrays

A TypedArray object describes an array-like view of an underlying binary data buffer and also provide a mechanism for reading and writing raw binary data in these memory buffers [4].

If we create a new instance of a typed array (i.e. aInt8Array), by passing in one of the above ArrayBuffers (to the typed array’s constructor) we will not actually consume too much additional internal memory. This is because typed arrays (when created by accepting an ArrayBuffer in their constructor) actually just reference the original ArrayBuffer; and operate on that array buffer address [5].

const needle = new Uint8Array(buffer_1);
const haystack = new Uint8Array(buffer_2);

The Uint8Array has a fill method which allows us to quickly populate all of the 1000000 individual bytes with a value between 0 and 255

needle.fill(111)
haystack.fill(222)

The contents for view_1 now looks something like this

[111, 111, 111, 111, 111, ... 111, 111, 111]

The contents for view_2 now looks something like this

[222, 222, 222, 222, 222, ... 222, 222, 222]

Keeping in mind that these values are just arbitrary for our demonstration. Yours would represent pixels of an image (from your application) or something of that nature. In the next section we will be sending the raw Javascript ArrayBuffers to the FaaS.

Call using ArrayBuffers

The FaaS, we are building here, is designed for very large byte arrays. After all, if we only had to compare 10 or 100 bytes, we would probably just iterate through them in Javascript. But if we are are planning on comparing, say, 1 million bytes then … surely we can afford to expend, say, just 10 measly bytes; in order to identify the length of the needle array. Right?

Instead of the complex encoding/decoding, we can just dedicate the first 10 bytes (of the single ArrayBuffer we are sending accross the Web) to represent the length of the needle (as single digit characters). When you think about it, this is quite simple; and trivial for Javascript to perform (on just 10 bytes). It is also certainly far less complicated for the developer (who has to implement this design), the compiler and ultimately the execution environment.

Given the above, let us consider implementing a very simple encoding and decoding design for this task.

In this example, let’s pretend that the length of the needle is 1 million bytes.

As you can see the first 10 bytes identify the needle_length. Seven of the ten available bytes have a single digit character in them. The remaining three are just zeroed out. Then, the needle (starting from the 11th byte) is exactly one million bytes long. The remaining bytes are the haystack. Simple right? Let’s implement (encode) that in Javascript, and then implement (decode) for the other side in Rust.

To implement the above design, first, we need to find the length of the needle and the haystack

needle_length = needle.length;
haystack_length = needle.length;
//1000000
//1000000

Then, we need to ensure that the needle length is not longer than what the first 10 bytes can hold (remembering that each of the 10 bytes only holds a single digit character). This maximum allowable amount 9999999999 is actually the byte based representation of 9.999999999 Gb which, let’s be honest, is more than enough head room for any byte array which is going to be sent over HTTP.

Remember also, that it is only the needle which is restricted to 9.9Gb. Technically, the haystack can be any length; as long as the needle is always smaller than the haystack. This is where the next step of making sure that the needle’s length is smaller than or equal to the haystack’s length comes in. Oh, and of course the needle_length can not be less than one.

if(needle_length < 1 || needle_length > 99999999 || needle_length > haystack_length)

If the needle_length qualifies, all we have to do, is convert the needle_length (which we calculated above 1000000) into the sequence of 10 bytes, so we end up with the first 10 elements of the new single byte array looking like this.

[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]

Of course the first 10 bytes can represent any number ranging from 1 to 9999999999.

Let’s take a look at the finished Javascript code which encodes our 2 ArrayBuffers and then calls the FaaS.

The Rust/Wasm (FaaS) side

Here is an example of the Rust logic which decodes the single array into a needle and the haystack.

We are meshing together the front-end and back-end code.

Performance comparison

The great news is that after implementing this FaaS, it is confirmed that the Wasm executed byte array searching outperforms the client-side Javascript execution, as shown in the graph below.

As you can see from the graph above, as the size of the needle gets bigger, the time taken to execute the code increases in a linear fashion for the client-side Javascript. The formal recording of testing data was stopped at 14500 needle size because the results were clear.

As testing exceeded needle sizes of 20000, the response times were ~30 seconds. As testing exceeded the needle size of ~50000 the client became unresponsive; the only way to continue was close and reopen the client.

In contrast, the FaaS execution remains constant. Even at needle sizes above 50000 (times were always sub 3 seconds). Any tiny fluctuations in the FaaS execution times are put down to differences in the network latency; as each HTTP request/response sends more than 1 million bytes (from the client to the FaaS). This is important to note. Even including the entire round-trip to and from the remote FaaS, the FaaS still outperforms the client-side Javascript (where the payload is always just computed locally).

The round trip for these tests stretched from Australia to Central USA and back. These tests would show the entire FaaS round trip to be significantly faster if the client were closer to the particular FaaS endpoint which was used.

This is a fair justification for outsourcing some compute intensive tasks from client-side Javascript to a FaaS such as SecondState’s FaaS. It also indicates the need for implementing FaaS edge computing infrastructure, in order to cut down on latency.

Please try out the demo below if you are in the USA (or anywhere in the world for that matter). Perhaps leave a comment to let us know how fast it was for you.

A live demo

You can test this out for yourself at this live demo URL.

Live demo available at < https://second-state.github.io/wasm-learning/faas/search-bytes/html/index.html >

If you are interested in testing out the client-side Javascript comparison (which the FaaS outperformed) feel free to paste the following code into your Chrome’s console etc.

Source: https://gist.github.com/tpmccallum/02eb723d4daad6b3a7b4eabee09d8c35

I did send out a Tweet asking for you awesome Javascript coders to send me your best code to ensure that this benchmarking is as competitive as possible.

If you are able to write client-side Javascript code which would be faster than what is being use here, please feel free to update the client-side Javascript code and let me know. I can then re-run the performance testing and update this article.

Also, if you would like any help with creating your own FaaS please get in touch.

Thanks for reading!

--

--

Timothy McCallum
Wasm
Editor for

I'm a technical writer and copy editor exploring WebAssembly (Wasm), software automation, and Artificial Intelligence (AI) while mastering Rust, Python, & Bash.