WebAssembly x Micro Frontends

Artur TakoevArtur Takoev
7 min read

Supercharge your web apps

Just like microservices made a revolution in backend architectures, micro frontends offer a modular approach to replace monolithic frontends. The ever-increasing use this cutting-edge tech has demonstrated in the last several years is totally deserved. Still, frontend architectures also don’t stand still, and their increasing complexity leaves the issue of efficient scaling of modular web apps in many respects a challenge.

My name is Artur, I’m a front-end developer (maybe just like you, since you’re reading my article!) In my practice of building heavy-load web apps, I have employed various technologies to manage this complexity. And today, I want to tell you about a technology I find one of the most promising – WebAssembly.

WebAssembly is an almost perfect tool for optimizing modular web apps – and in my opinion, especially so in combination with micro frontends. It is modular in nature, which aligns well with micro frontends and provides high performance for resource-intensive apps. However – as usual! – there are issues that complicate this integration. In this article, I will dissect some of these issues (that I encountered and dealt with myself) and give you hints and insights on how to marry these two brilliant technologies into your projects.

Issue 1: Language Interoperability

The Challenge

This is the first issue that you will encounter, as it’s the most basic one. WebAssembly modules are – by design – written in low-level languages like C++ or its modern contestant Rust (my personal preference). Micro frontends, on the other hand, are pieces of JavaScript, HTML, and CSS built with some JavaScript framework. These environments have differing data structures, type systems, and runtime behavior, and making them work together requires effort.

Expect to encounter issues like mismatched data types and the need to convert data back and forth between WebAssembly and JS. This data conversion adds overhead, spawning performance issues and demanding extra coding. Additionally, WebAssembly lacks direct access to the JavaScript DOM model, which forces us to look for more efficient ways for intercommunication between WebAssembly modules and JS components.

The Cure

Luckily, there are more than enough ways to mitigate this issue, both language-specific and more universal. wasm-bindgen can automatically generate glue code for Rust and JavaScript. Use the #[wasm_bindgen] macro to annotate your Rust functions, and it will expose them to JS. For instance, this function:

#[wasm_bindgen]

pub fn greet(name: &str) -> String {

format!("Hello, {}!", name)

}

generates a JS wrapper enabling neat function calls like this:

import { greet } from './pkg/my_wasm_module';

console.log(greet('WebAssembly'));

Similarly, Emscripten transpiles C++ code into WebAssembly while generating necessary JS bindings. Let’s annotate a C++ function with EMSCRIPTEN_KEEPALIVE to make it accessible in JS:

#include <emscripten/emscripten.h>

extern "C" {

EMSCRIPTEN_KEEPALIVE

int add(int x, int y) {

return x + y;

}

}

– which we can easily access in JS via:

Module.onRuntimeInitialized = () => {

console.log(Module._add(3, 4));

};

A more universal approach is using the WebAssembly System Interface. It standardizes system calls and further abstracts common operations like file I/O and networking. System Interface wasn’t developed with micro frontends in mind, yet it provides us with an elegant way to handle system operations, improving module portability.

Or you can go from the JavaScript end and directly use its ArrayBuffer or SharedArrayBuffer objects to access WebAssembly's memory, minimizing data conversion overhead. For instance, by creating a typed view of the buffer:

const memory = new WebAssembly.Memory({ initial: 1 });

const int32View = newInt32Array(memory.buffer);

you will enable WebAssembly to write directly into this memory space, creating a shared data region and allowing data to be exchanged without unnecessary copying. One problem solved!

Issue 2: State Management

The Challenge

Your next major challenge will be state management. WebAssembly uses an isolated environment – which is good for security but complicates direct state sharing between modules. This gets even more pronounced if you use different JS frameworks for different micro frontends in your project. Unlike JavaScript apps that benefit from centralized state management tools (e.g., Redux or Context API in React), WebAssembly modules typically handle state internally. This leads to state data fragmentation and makes synchronization trickier than it ideally should be. (I know, the world is not ideal.)

And as if that wasn’t enough, you will almost certainly get an extra bottleneck in the form of the overhead of serialization and deserialization between two memory models. This issue will be more pronounced in modular architectures in which various frontend microservices interact with shared WebAssembly components.

The Cure

For effective state management, you can implement a unified global state store. Redux or MobX are good options I use myself. For instance, in Redux, create an action that WebAssembly can dispatch via JS functions:

const updateState = (payload) => ({

type: 'UPDATE_STATE',

payload

});

function dispatchUpdate(payloadPointer, length) {

const data = new Uint8Array(memory.buffer, payloadPointer, length);

store.dispatch(updateState(data));

}

Another possible strategy is to design a specialized API that will manage the communication between WebAssembly and JS, and handle serialization and deserialization internally. Write a JavaScript wrapper and expose your functions to WebAssembly modules:

function updateFrontendState(data) {

// Handle state update logic

globalState = processWasmData(data);

}
And again, just like with our issue 1 above, you can use ArrayBuffer to map WebAssembly memory directly to JavaScript and let them work with shared data. Let me expand on the previous example:

const memory = new WebAssembly.Memory({ initial: 4 });

const sharedInt32View = new Int32Array(memory.buffer);

// Populate the shared memory with some values sharedInt32View[0] = 42;

sharedInt32View[1] = 100;

Issue 3. Performance Overhead

The Challenge

The third and final issue I will review here is the problem of performance overhead that occurs because of the differences in execution models. WebAssembly requires switching contexts when calling functions back and forth with JS, and this ‘back-and-forth’ adds latency, potentially causing serious performance degradation. Besides, frequent conversion between JS objects and WebAssembly’s memory format is an extra processing burden your app doesn’t need.

And now we come to that ‘almost perfect’ part I mentioned earlier: WebAssembly doesn’t have garbage collection and uses a linear memory model. Because of this, you will have to be extra careful with memory allocation and deallocation, risking memory leaks and excessive heap fragmentation. Memory usage and execution scheduling with multiple WebAssembly modules in a modular environment will also add headaches, further complicated by typical micro frontend issues like lazy loading, network requests, and independent module life cycles.

The Cure

To reduce performance overhead, you will need to find ways of optimizing data exchange between WebAssembly and JavaScript. Below are some hints from my personal experience dealing with this combination.

First of all, to decrease the latency minimize the number of function calls across the boundary. Group calls together and/or pass data in bulk for processing, e.g. using the same typed arrays Int32Array or Float64Array.

Next, design your WebAssembly modules with clearly defined APIs that provide a minimal surface area. This way, you will reduce the need for frequent back-and-forth function calls and simplify interactions. Use WebAssembly's strengths to implement complex algorithms for computationally intensive tasks and leave simple logic to JavaScript.

Be careful when designing memory allocation strategies, and you will avoid the worst of the WebAssembly memory management issues. wasm-bindgen and Emscripten I mentioned above will help you with efficient allocation management. And don’t forget to include regular memory usage inspections in your routines to detect leaks and fragmentation. Here, you can use browser-based profiling tools or better – the built-in WebAssembly memory inspector.

Last but not least, make sure your WebAssembly modules’ execution aligns with the asynchronous nature of micro frontends. Apply lazy loading to load modules only when you need them and minimize blocking network requests.

Conclusion

Of course, this article is not enough to cover all the issues you may possibly encounter implementing WebAssembly with micro frontends. You will have to deal with loading and caching challenges – module caching strategies may inadvertently introduce inconsistencies or slow loading times. And although WebAssembly utilizes an isolated environment, naturally, this won’t magically solve all security issues! However, I decided to leave these aspects outside the scope of my article, as each of them requires a separate and thorough review. Still, I hope the solutions described above will help you in your work and let you utilize this great combination to the fullest of its power!

PS: If I got you interested today and you’d like to dig deeper into this matter, I recommend you look into standardized patterns for secure WebAssembly module loading, especially in distributed architectures with multiple interacting micro frontends (oomph!) Another area I find interesting is devising ways to further streamline data exchange and state management between WebAssembly and JavaScript. In my current job, I build real-time updates-hungry apps, and this issue is currently at the center of my attention.

0
Subscribe to my newsletter

Read articles from Artur Takoev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Artur Takoev
Artur Takoev