Nowadays, web-apps are expected to be more and more powerful. Since the conception of AJAX, the web is no longer a medium to simply view HTML documents. People are building an actual full-fledged app to run on web browsers! These are made possible by the ever-growing list of new web APIs, the massive amount of modules written by the community, and the collaboration between browser vendors.
The Goal 🥅
Our goal was to build a QR scanner for our mobile web. Many e-commerce platforms have QR scanner. Tokopedia has had one in their native mobile apps for a long time, but not in the mobile web. The reason was not that it was not possible to implement. There are many libraries out there written by the community that handle this, but because image processing is an inherently resource-intensive task, it is hard to maintain a good user experience while doing it. So, we used this opportunity as an excuse to start looking into WebAssembly.
WebAssembly (Wasm) 🛠
[WebAssembly] provides a way to run code written in multiple languages on the web at near native speed, with client apps running on the web that previously couldn’t have done so. — MDN
WebAssembly allows us to take a code written in another language, compile it to
.wasm, not just C, C++ and Rust. There is even something called AssemblyScript, which allows you to write TypeScript and compiles to WebAssembly! You can check out the long list of languages here.
The Experiment 🤔
.wasm file and a
.js file containing glue codes for interacting with the
The chart shows the time it took to do QR-decoding on an image on a MacBook Pro 2018, with 6x CPU slowdown. jsQR took an average of around 47ms, while quirc.wasm took an average of around 29ms.
Another interesting thing is, in some cases, our scanner that was using jsQR jumped to a whopping 1023ms! It then averaged at around 800ms.
Finally, these numbers also translate to the FPS we get.
We get around 34 FPS on quirc.wasm and around 17 FPS on jsQR. The numbers of course will vary depending on the device and browser, but I think we can safely conclude that doing QR decoding with WebAssembly will yield better performance.
Offloading it to Web Worker 👷
Web Workers are a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface. — MDN
Using web workers, we can do task in a separate thread so the main thread does not get fully occupied with all tasks. This concept is similar with what native apps have been doing, having a main UI thread to handle UI, and handling other tasks in separate thread. Here is another great talk about why you should try finding things to offload to workers: The main thread is overworked and underpaid (Chrome Dev Summit 2019).
To achieve 60 FPS, the main thread has to ship frames every 16.7 ms. This is very hard to do especially if the main thread has to do QR decoding as well. Using WebAssembly helped making the process fast enough, but it still runs on the main thread.
We ran emscripten again with different options so the output can be run in workers. To make it easier to use in our webpack-built-app, we used comlink-loader. It allowed us to have an easy-to-use API when working with workers.
If you are looking to use workers in your web-app, the author (Jason Miller) suggested to use
comlinkinstead. The reason we are still using
comlink-loaderis only for the inline capability.
The Results 📈
Doing the QR decoding with WebAssembly +Web Worker allows our main thread to be less occupied. For every decoding, the main thread only took around 6ms. Note that the main thread does not actually do any decoding. It just passes the image data to the worker and receives the result. Overall, the QR decoding is slightly slower because of the overhead of passing around data between the main thread and the worker thread. But, the benefit of having our main thread less busy is worth the trade. It allowed us to achieve 60 FPS for our QR scanner! 🎉
Having WebAssembly and Web Worker in our arsenal can really help us build powerful web-apps. We can do a lot of interesting stuffs in browsers, while still keeping the user experience great. The great tools available today such as emscripten, comlink, worker-plugin, comlink-loader, and webpack really help making the experience of doing all these pleasant! If you have not played around with WebAssembly and/or Web Worker before, there hasn’t been a better time to start doing so!
- WebAssembly for Web Developers
- V8.dev site
- Launching Ignition and Turbofan
- Understanding V8’s Bytecode
- DLS Keynote: Ignition: Jump-starting an Interpreter for V8
- The main thread is overworked and underpaid (Chrome Dev Summit 2019)