Story Melange
  • Blog
  • Projects
  • Archive
  • About
  • Subscribe

On this page

  • 1 C++ for the Masses
  • 2 The color contrast grid
  • 3 Getting C++ Data into the Browser Window
  • 4 Webassembly Limitations
    • 4.1 Resources
    • 4.2 Webhosting
  • 5 Summary

C++ in your Browser. Is WebAssembly worth the effort?

C++
Applied Engineering
Software Engineering
My experiment of getting C++ delivered to the browser. Find out the why and see why client-side compute is cheap and expensive at the same time.
Author

Dominik Lindner

Published

November 19, 2025

1 C++ for the Masses

C++ is often treated as a legacy programming language. Or something you only need when you are worried about low level system performance. With WebAssembly, that boundary is shifting.

The benefit of Webassembly: free client side compute. You wrap you costly backend logic and ship it to the client.

While I have been familiar with the idea, I wanted to see how this turns out in practice.

Spoiler. Yes compute is free, development time isn’t. You can see the solution here

2 The color contrast grid

I built a small program that arrangers colors in a grid to maximize contrast between neighbors. While simple, the search space is fast and optimization algorithms are difficult to implement.

colors.jpeg

Besides brute force, I tried hill climbing and simulated annealing. But the problem is actually a little bit more complex than I thought and would need more time.

Therefore, I focused on straightforward C++ improvement: inlining of functions, avoiding to reallocate memory inside loops and lookup tables. The speedups are between 15% to 30%.

3 Getting C++ Data into the Browser Window

WebAssembly offers a way to run high-performance code safely in the browser. Using Emscripten, I compiled the solver into a module and connected it to a small TypeScript UI.

Biggest lesson learned: complex objects cannot cross the boundary by value.

Simply evoking call by value functions of complex objects lead to memory leaks. We can use shared memory buffers to avoid these leaks.

We need

  • a fixed buffer in Typescript
  • a C++ function to fill the buffer
  • another Typescript function to read from it

I implemented this using an additional webworker, as I run several grids at the same time

Buffer declaration in the main.ts

// We first declare the buffer and allocate memory
const canvasRGBBuffers: SharedArrayBuffer[] = [];  
const canvasRGBViews: Uint8Array[] = [];

canvasRGBBuffers[canvasIndex] = new SharedArrayBuffer(dim * dim * 3); 
canvasRGBViews[canvasIndex] = new Uint8Array(rgbSAB);

C++ Function

// In my example I have a global object storing pointers to the algorithms
std::unordered_map<int, std::unique_ptr<Algorithm> > algos;

// This is the actual function that reads the grid
void export_grid_rgb(int id, std::uint8_t *out, int max_len) {
// get grid from the algorithm
    auto &algo = *algos.at(id);  
    auto *grid = algo.getBestGrid();
...
}

// Emscripten Binding to export to typescript
function("export_grid_rgb",  
         optional_override([](int id, uintptr_t ptr, int len) {  
             auto *buf = reinterpret_cast<std::uint8_t *>(ptr);  
             export_grid_rgb(id, buf, len);  
         })  
);

Typescript Invocation in the worker

// Receive callback in worker, receives the memory and then forwards the pointer
self.onmessage = async (ev) => {  
    const {  
        sab,   
    } = ev.data;
const createModule = (await import(scriptUrl)).default;  
wasmModule = await createModule();


const sharedRGB = new Uint8Array(sab);
const rgbLen = sharedRGB.length;  
const wasmRGBPtr = wasmModule._malloc(rgbLen);
wasmModule.export_grid_rgb(canvasIndex, wasmRGBPtr, rgbLen);
}

Typescript main

// Send command in main
worker.postMessage({  
    sab: canvasRGBBuffers[canvasIndex],  
});

// Receive callback in main only uses the view on the buffer
worker.onmessage = (ev) => {  
...
  
    const rgbView = canvasRGBViews;  
    drawGridFromRGB(canvas, rgbView, dim);
    ...
}

4 Webassembly Limitations

4.1 Resources

I discovered there are resource limitations.

  • 16GB of memory
  • Main thread wasm must yield back every 30 odd seconds to avoid dialogs
  • Worker threads may be put to sleep if window not in focus

However, that is the size of a small cloud virtual PC. Another aligned technology that allows the usage of gpu is WebGPU. For an intro to both, see this.

Currently GPU access in Webassembly is only possible via the JS-apis. But once direct access is possible, there could be even better performance gains.

4.2 Webhosting

To use the webworkers, we require threads. And to use those we need special CORS headers. These headers are not available on Github Pages, why I needed to host the project on Cloudflare. For hobbiest, that just one extra layer.

5 Summary

As you can see getting something to work in WebAssembly is requires quite a lot of boilerplate code. As the technology is less used LLMs are less of a help. While I used LLMs to create this examples a lot of manual effort was necessary.

For many prototypes, this effort would be better directed into the value adding activities.

This leads to a dilemma: say you have a business idea that would work well if the cloud compute would not be ruining the business case. WebAssembly could make it fly, but to get to working prototype you would be better off developing a FastApi Backend with Python and some pybind bindings for the C++ part.

Some Example Ideas

  • Scientific compute with free tier computations
  • Client Side ML of SLMs or voice transcription

But I guess that is always the case with infrastructure technology. There is always an upfront cost that needs to be paid.


Source Code is available here

Project is live here

Like this post? Get espresso-shot tips and slow-pour insights straight to your inbox.

Comments

Join the discussion below.


© 2025 by Dr. Dominik Lindner
This website was created with Quarto


Impressum

Cookie Preferences


Code · Data · Curiosity
Espresso-strength insights on AI, systems, and learning.