DEV Community

Cover image for Can WebAssembly make your web apps faster?
yusuf
yusuf

Posted on

Can WebAssembly make your web apps faster?

What is WebAssembly?

Assembly?

As we all know, machines read binary codes (strings of zeros and ones) because it is very convenient for them but not for humans. So engineers basically created a mapping from binary code and called the mapped version 'Assembly'.

Assembly and binary code

WebAssembly?

WebAssembly is an 'Assembly'-like language. It's nickname is wasm. Basically WebAssembly is the Assembly language for modern browsers.

People usually don't write Assembly code directly because it can be tedious. That's why there are many Assembly compilers that simply create Assembly code from C/C++, C#, Rust, Java ... So basically you can run an existing C/C++ or Rust application inside a browser.

WebAssembly code can be directly injected into Javascript or Node.js environment. This can be very useful because you can combine WebAssembly with Javascript.

WebAssembly provides near native performance. Javascript is just-in-time (JIT) compiled. But WebAssembly is pre-compiled. So that's why wasm can be executed faster than Javascript code.

Get your hands dirty

Firstly, all the source codes used in this post available in GitHub. The ultimate question in my mind is "Can we sort an array of integers faster with WebAssembly?". Since we want to execute faster, I think I should use the holy programming language C.
To compile WebAssembly, I used emscripten I wrote C code and compiled C code into WebAssembly using emscripten. I simply followed instructions at MDN and emscripten

Hello World!

After you download and install emscripten by following their documentation, you should have a emsdk folder. Go into that folder and run source ./emsdk_env.sh. Now you can compile your C codes into WebAssembly. To compile a hello world WebAssembly code, I used command emcc -o hello3.html hello3.c --shell-file html_template/shell_minimal2.html -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']". Here emcc is the emscripten compiler. -o hello3.html hello3.c means compile 'hello3.c' and output HTML file 'hello3.html'. My 'hello3.c' is like below

#include <stdio.h>
#include <emscripten/emscripten.h>

int main() {
    printf("Hello World\n");
    return 0;
}

#ifdef __cplusplus
#define EXTERN extern "C"
#else
#define EXTERN
#endif

EXTERN EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) {
    printf("MyFunction Called\n");
}
Enter fullscreen mode Exit fullscreen mode

It simply prints 'Hello World' to the developer console when the web page is loaded. And also prints 'MyFunction Called' when the C function is called.

--shell-file html_template/shell_minimal2.html of the script basically uses a template html file to generate the output 'hello3.html' file. My 'shell_minimal2.html' is like below.

<html>
  <body>
    <button id="mybutton">Call the C function</button>
  </body>
</html>
{{{ SCRIPT }}}
<script>
  document.getElementById("mybutton").addEventListener("click", () => {
    alert("check console");
    const result = Module.ccall(
      "myFunction", // name of C function
      null, // return type
      null, // argument types
      null // arguments
    );
  });
</script>
Enter fullscreen mode Exit fullscreen mode

The rest of the command is necessary to call the function using some special method called ccal. If you execute the command, it will generate 'hello3.html', 'hello3.js', and 'hello3.wasm' files. If you look at 'hello3.html' you will see

<html>
  <body>
    <button id="mybutton">Call the C function</button>
  </body>
</html>
<script async type="text/javascript" src="hello3.js"></script>
<script>
  document.getElementById("mybutton").addEventListener("click", () => {
    alert("check console");
    const result = Module.ccall(
      "myFunction", // name of C function
      null, // return type
      null, // argument types
      null // arguments
    );
  });
</script>
Enter fullscreen mode Exit fullscreen mode

The only difference between the template file 'shell_minimal2.html' and 'hello3.html' is the {{{ SCRIPT }}} section. Inside the 'hello3.html', this part is replaced with <script async type="text/javascript" src="hello3.js"> 'hello3.js' is a Javascript file that provides a global variable named Module and also imports hello3.wasm. hello3.wasm is a binary file. You cannot read it with naked eye easily. Start an HTTP server and open 'hello3.html' in a modern browser. I'm using VSCode for code editing and it has a nice extension for simple HTTP server. Then you can actually see that wasm file is converted to WebAssembly Text (.wat) format in your browser.
WebAssembly Text representation in browser
In developer console, you can see printf statements of C codes are actually printed. This is hello world for WebAssembly! Also if you click to the button, you will see it calls a C function! That's amazing! From JavaScript we can call a C function!
Image description

Fibonacci

Now let's see if a C code can execute faster than plain Javascript code. To make a comparison I implemented fibonacci algorithm in a very inefficient way in both C and Javascript and executed side-by-side.

I executed command just like hello world example emcc -o hello4.html fib.c --shell-file html_template/simple_compare.html -sEXPORTED_FUNCTIONS=_fib -sEXPORTED_RUNTIME_METHODS=cwrap. Here to call a C function, we use someother special method cwrap.
Below is my C code file 'fib.c'

int fib(int n)
{
    if (n < 2)
        return n;
    return fib(n - 1) + fib(n - 2);
}
Enter fullscreen mode Exit fullscreen mode

Here is my 'simple_compare.html' file

{{{ SCRIPT }}}
<input type="number" id="fibNum" value="35" />
<button id="mybutton">Compare JS vs C on Fibonacci</button>
<script>
  document.getElementById("mybutton").addEventListener("click", () => {
    const fibIndex = Number(document.getElementById("fibNum").value);
    const t1 = executeFibonacciOnC(fibIndex);
    const t2 = executeFibonacciOnJS(fibIndex);
    console.log("C time:", t1, "JS time:", t2);
  });

  // finds the 'n'th fibonacci number in C using the worst implementation and returns the execution time
  function executeFibonacciOnC(n) {
    fibC = Module.cwrap("fib", "number", ["number"]);
    const t1 = performance.now();
    const res = fibC(n);
    const t2 = performance.now();
    console.log("c result: ", res);
    return t2 - t1;
  }

  function executeFibonacciOnJS(n) {
    const t1 = performance.now();
    const res = fib(n);
    const t2 = performance.now();
    console.log("JS result: ", res);
    return t2 - t1;
  }

  function fib(n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
  }
</script>

Enter fullscreen mode Exit fullscreen mode

Here you can see that Javascript is a lot faster than C code. I feel like I wasted all my time and WebAssembly is just a balloon.
C vs JS on Fibonacci calculation without any compiler optimizations

Then I realized there is some compiler optimization flags in emscripten. Let's use them emcc -o hello4.html fib.c --shell-file html_template/simple_compare.html -sEXPORTED_FUNCTIONS=_fib -sEXPORTED_RUNTIME_METHODS=cwrap -O2 I just added a -O2 flag to my command and do it again.

Now you can see some magic!
C vs JS on Fibonacci calculation with O2 compiler optimization flag
This time, C code executes faster! In some cases it is like even 2 times faster!

Sort numbers

Now let's try our ultimate aim. Can we sort numbers faster than Javascript? Let's try with C standard library function qsort I think it should be faster, C codes are usually fast. Similar to previous command, I executed command emcc -o qsort.html arraySorter.c --shell-file html_template/simple_compare_array.html -sEXPORTED_FUNCTIONS=_arraySorter,_malloc,_free -sEXPORTED_RUNTIME_METHODS=cwrap My 'arraySorter.c' is very simple like below. It simply calls qsort function with a comparer function.

#include <stdlib.h>

int compareFn(const void *a, const void *b)
{
    return (*(int *)a - *(int *)b);
}

void arraySorter(int *arr, int size)
{
    qsort(arr, size, sizeof(int), compareFn);
}
Enter fullscreen mode Exit fullscreen mode

My template file 'simple_compare_array.html' is like below. Here passing and array as a parameter is bit hard. We need to use malloc and free functions to create and array and pass it to C.

{{{ SCRIPT }}}
<input type="number" id="arrSize" value="1000000" />
<button id="mybutton">Compare JS vs C on sorting integer array</button>
<script>
  function getArray() {
    const l = Number(document.getElementById("arrSize").value);
    return Array.from({ length: l }, () => Math.floor(Math.random() * l));
  }
  document.getElementById("mybutton").addEventListener("click", () => {
    const arr1 = getArray();
    const arr2 = Array.from(arr1);
    const t1 = arrayOperationsOnC(arr1);
    const t2 = arrayOperationsOnJS(arr2);
    console.log("C time:", t1, "JS time:", t2);
  });

  function arrayOperationsOnC(n) {
    const BYTE_SIZE_OF_INT = 4;
    const t1 = performance.now();
    const arraySize = n.length;
    const arrayPointer = Module._malloc(arraySize * BYTE_SIZE_OF_INT);
    Module.HEAP32.set(new Int32Array(n), arrayPointer / BYTE_SIZE_OF_INT);
    const cFunc = Module.cwrap("arraySorter", null, ["number", "number"]);
    cFunc(arrayPointer, arraySize);
    const resultArray = Array.from(
      Module.HEAP32.subarray(
        arrayPointer / BYTE_SIZE_OF_INT,
        arrayPointer / BYTE_SIZE_OF_INT + arraySize
      )
    );
    console.log(resultArray);
    Module._free(arrayPointer);
    const t2 = performance.now();
    return t2 - t1;
  }

  function arrayOperationsOnJS(n) {
    const t1 = performance.now();
    const res = arraySorter(n);
    console.log(res);
    const t2 = performance.now();
    return t2 - t1;
  }

  function arraySorter(arr) {
    return arr.sort((a, b) => a - b);
  }
</script>
Enter fullscreen mode Exit fullscreen mode

If I execute this I see my C code is like 4 times slower!
What the heck is wrong?

Image description

OK I see I didn't use compiler optimization flags. Let's try -O2 flag and do it again. Now it is faster but still very slower than Javascript.

C time: 716.1 JS time: 269.5

Even if I use -O3 flag, I see it is still slower. There is no '-O4' this is the most optimized.

C time: 711.9 JS time: 270.6

Now we are sure that quick sort with C cannot pass plain Javascript. But we used C standard library function qsort. Can we try plain C code for quick sort? Let's try. I used command emcc -o faster_sorter.html arraySorter2.c --shell-file html_template/simple_compare_array.html -sEXPORTED_FUNCTIONS=_arraySorter,_malloc,_free -sEXPORTED_RUNTIME_METHODS=cwrap -O2 Here I used the same HTML template but this time I implemented quick sort algorithm with plain C code. Below in my arraySorter2.c file.

// Quick sort in C

// function to swap elements
void swap(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

// function to find the partition position
int partition(int array[], int low, int high)
{

    // select the rightmost element as pivot
    int pivot = array[high];

    // pointer for greater element
    int i = (low - 1);

    // traverse each element of the array
    // compare them with the pivot
    for (int j = low; j < high; j++)
    {
        if (array[j] <= pivot)
        {

            // if element smaller than pivot is found
            // swap it with the greater element pointed by i
            i++;

            // swap element at i with element at j
            swap(&array[i], &array[j]);
        }
    }

    // swap the pivot element with the greater element at i
    swap(&array[i + 1], &array[high]);

    // return the partition point
    return (i + 1);
}

void quickSort(int array[], int low, int high)
{
    if (low < high)
    {

        // find the pivot element such that
        // elements smaller than pivot are on left of pivot
        // elements greater than pivot are on right of pivot
        int pi = partition(array, low, high);

        // recursive call on the left of pivot
        quickSort(array, low, pi - 1);

        // recursive call on the right of pivot
        quickSort(array, pi + 1, high);
    }
}

void arraySorter(int *arr, int size)
{
    quickSort(arr, 0, size - 1);
}
Enter fullscreen mode Exit fullscreen mode

Now it seems with -O2 flag we can sort integers faster than plain Javascript! Now C code is like 2 times faster. That's amazing!

C time: 156.8 JS time: 271.5

Is Wasm faster in your browser? Try and see now. All the source codes is available under MIT license.

Top comments (0)