As we find ourselves firmly in the middle of 2020 (what a year so far, right?), web developers have spent several years working with WebAssembly (WASM). What began as an exciting experiment has now matured into a robust, production-ready technology with widespread browser support and growing adoption across the industry.
The Performance Promise
JavaScript has served us well for decades, but we’ve all hit that performance wall, the point where no amount of optimization seems to help. Whether you’re building data visualization tools, complex animations, or processing user-generated content, JavaScript’s interpreted nature introduces unavoidable overhead.
Enter WebAssembly (or Wasm), a binary instruction format that runs at near-native speeds in the browser. Now with over three years of development and refinement, WASM has proven its capability to deliver predictable, high-performance execution across all modern browsers since becoming a W3C recommendation in December 2019.
But is the performance gain worth the added complexity? Let’s find out by diving into a small example.
Image Processing: A Perfect Test Case
Image manipulation is a classic example of computationally intensive work when implemented in pure JavaScript. Let’s build a simple brightness adjustment function in both JavaScript and WebAssembly to see the difference.
The JavaScript Approach
Here’s how we might adjust the brightness of an image using vanilla JavaScript:
function adjustBrightness(imageData, intensity) {
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = Math.min(255, pixels[i] + intensity); // Red
pixels[i+1] = Math.min(255, pixels[i+1] + intensity); // Green
pixels[i+2] = Math.min(255, pixels[i+2] + intensity); // Blue
// Alpha channel (i+3) remains unchanged
}
return imageData;
}
Simple enough, right? For each pixel in our image, we increase the RGB values by the specified intensity, making sure not to exceed 255.
The WebAssembly Approach
To leverage WebAssembly, I’ll use C and Emscripten, which has matured nicely since its early days. If you haven’t set up Emscripten yet, the official documentation provides clear installation instructions.
Here’s our C implementation:
// brightness.c
#include <emscripten.h>
#include <stdint.h>
EMSCRIPTEN_KEEPALIVE
void processBrightness(uint8_t* pixels, int length, int intensity) {
for (int i = 0; i < length; i += 4) {
int r = pixels[i] + intensity;
int g = pixels[i+1] + intensity;
int b = pixels[i+2] + intensity;
pixels[i] = (r > 255) ? 255 : r;
pixels[i+1] = (g > 255) ? 255 : g;
pixels[i+2] = (b > 255) ? 255 : b;
// Alpha remains unchanged
}
}
Compile this to WebAssembly using Emscripten:
emcc brightness.c -o brightness.js -s WASM=1 -s EXPORTED_FUNCTIONS="['_processBrightness']" -O3
This command generates both brightness.js (glue code) and brightness.wasm (the actual WebAssembly module).
Integrating WebAssembly Into Your App
Now let’s bring everything together:
// Load the WebAssembly module
let wasmModule;
fetch('brightness.wasm')
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer))
.then(module => {
wasmModule = module.instance;
console.log('WebAssembly module loaded!');
enableUI(); // Enable our UI now that everything is loaded
});
// Process with WebAssembly
function processImageWithWasm(imageData, intensity) {
const pixels = imageData.data;
// Get the memory from the WASM module
const memory = wasmModule.exports.memory;
// Allocate memory in the WebAssembly instance
const ptr = wasmModule.exports.malloc(pixels.length);
// Create a view into WebAssembly memory
const heap = new Uint8Array(memory.buffer, ptr, pixels.length);
// Copy image data to WebAssembly memory
heap.set(new Uint8Array(pixels.buffer));
// Call the WebAssembly function
wasmModule.exports.processBrightness(ptr, pixels.length, intensity);
// Copy the result back
pixels.set(heap.subarray(0, pixels.length));
// Free the allocated memory
wasmModule.exports.free(ptr);
return imageData;
}
The Performance Showdown
I tested both implementations on a variety of images and measured execution times. The results speak for themselves:
- JavaScript Processing: 200-300ms for a 1080p image
- WebAssembly Processing: 40-60ms for the same image
That’s a 4-5x performance improvement! And this is for a relatively simple operation, the gap widens further with more complex algorithms like convolutions or Fourier transforms.
Real-world Considerations
While WebAssembly clearly wins the performance race, there are trade-offs to consider:
The Good
Predictable Performance: WebAssembly execution times are more consistent, making it ideal for applications requiring smooth user experiences.
Language Choice: You’re not limited to JavaScript anymore. C, C++, Rust, and even Go can now be part of your web development toolkit.
Mature Ecosystem: After three years of widespread browser support (Firefox and Chrome in 2017, followed by Edge and Safari), WebAssembly now benefits from established toolchains, comprehensive documentation, and growing community knowledge.
The Challenges
Development Complexity: The build process is more involved, and you’ll need to be comfortable with lower-level languages.
Debugging: While tools like Chrome DevTools are adding WebAssembly support, debugging is still more challenging than with JavaScript.
Memory Management: Working directly with memory requires careful management to avoid leaks and fragmentation.
Who’s Using WebAssembly Today?
The adoption of WebAssembly continues to grow:
Figma leverages WebAssembly for their design tool, achieving desktop-quality performance in the browser.
Google Earth uses WebAssembly to deliver their full 3D mapping experience on the web.
AutoCAD Web brings professional CAD capabilities to browsers through WebAssembly.
Squoosh (Google’s image compression app) demonstrates impressive performance gains in image processing.
Getting Started Today
If you’re intrigued and want to start exploring WebAssembly, here’s a modern workflow that works well:
Choose your language: Rust is gaining popularity for WebAssembly development thanks to its memory safety and performance, but C/C++ with Emscripten remains a solid choice.
Tooling:
- wasm-pack for Rust developers
- Emscripten for C/C++ developers
- AssemblyScript for TypeScript developers wanting an easier entry point
Learn the basics: Mozilla’s WebAssembly documentation provides an excellent overview.
Start small: Begin by porting small, performance-critical functions rather than entire applications.
Conclusion
WebAssembly is proving itself as more than just a JavaScript replacement, it’s a complementary technology that enables new capabilities for web applications. While it’s not the right solution for every problem, it’s an invaluable tool for performance-critical tasks.
The ecosystem is rapidly evolving, with new tools and frameworks appearing regularly. Projects like the WebAssembly System Interface (WASI) are expanding its potential beyond browsers, hinting at a future where WebAssembly becomes a universal runtime.
For now, though, if your web application is hitting JavaScript performance limits, especially with data processing, graphics, or complex algorithms. It’s a perfect time to explore what WebAssembly can do for you.
Happy coding, and stay safe in these unusual times!