Welcome to the first entry in our series, Porting Native Apps to the Web with WebAssembly: A Complete Guide!
In this series, you’ll discover how WebAssembly (Wasm) can help transform desktop or native apps into fast, capable web applications. We’ll explore the fundamentals of Wasm, discuss architectural considerations, and provide practical strategies for porting native applications to the web with Wasm.
Whether you’re an engineering leader, architect, or developer, this guide will help you develop the right mental models to approach a porting effort and to set you up for success.
To start, let’s look at the history and design of WebAssembly, its role in modern web browsers, and why it’s a game-changer for web applications.
The web’s ubiquity has driven a demand for increasingly advanced applications in the browser. In response, browsers have continuously evolved, adding new capabilities to match the growing complexity of modern web apps.
However, JavaScript — the web’s only built-in programming language — has historically posed challenges to this advancement. Among other limitations, it limits the ability for web apps to leverage existing software and libraries developed in languages other than JavaScript.
This led to the “compile-to-JavaScript” movement that started in the 2010s — an ecosystem of tools and techniques aimed at compiling code written in other programming languages to JavaScript, making it runnable in the browser.
Among the compile-to-JavaScript technologies that emerged, one of the most prominent was Asm.js — a well defined subset of JavaScript carefully designed to be an efficient compilation target for software written in languages like C/C++.
By using only specific JavaScript features, Asm.js could provide significant performance benefits over hand-written JavaScript. For the first time, C/C++ code could be compiled to Asm.js and run in the browser, and provide better performance than JavaScript code. This unlocked a new world of interesting use cases. The impact of Asm.js, driven by its novel design, was so significant, that it prompted some browsers to develop specific optimizations 1,2 for Asm.js code.
Asm.js demonstrated the potential of compiling native code for the web, but its reliance on JavaScript ultimately limited its capabilities. Despite its clever optimizations to achieve impressive performance, it became clear that a faster, more flexible, more capable and more long-term solution could be realised by a purpose built technology — one that could take inspiration from the design of Asm.js, but was not constrained to JavaScript as its runtime 3, and that could be standardised and widely adopted at the browser level 4.
In 2015, WebAssembly (Wasm) was announced as a cross browser initiative to develop and standardise a purpose-built, assembly-like language for web browsers, with the express goal of being able to run code compiled from other languages in the browser.
Today, Wasm is supported by all major browsers, has become an open standard, and has a rapidly growing ecosystem of tools and technologies built around it. Its standardisation ensures longevity, stability and a commitment to backward compatibility, making it an excellent platform on which to base architectural decisions today.
As of January 2025, WebAssembly is used on over 4.5% of websites with adoption growing exponentially. Major companies — including Adobe, Figma, AutoCAD, Google, Microsoft, Shopify and more — are investing heavily in WebAssembly, and using it to power some of their most widely used products.
Wasm is a low level, assembly-like language optimised for safe and efficient execution in modern web browsers. Its binary instruction format prioritises compact, fast, and portable code that can achieve near native performance5.
On the browser, Wasm is designed to run alongside and inter-operate with JavaScript, extending the possibilities for client-side applications.
We will review its design at a high level here, but you can find full design details at webassembly.org.
Wasm is a bytecode — a set of instructions in a binary format that runs on a virtual machine (VM). All modern browsers today include a mature Wasm VM by default.
It is conceptually similar to other languages that have a bytecode executed in a virtual machine (e.g. Java or .NET), with the major difference being that Wasm code is not constrained to any single source language; it can be generated from a wide variety of languages.
Wasm code can be disassembled into a text representation (WebAssembly Text Format, or WAT):
;; Module start
(module
;; Add function
(func $add (param i32 i32) (result i32)
local.get 1
local.get 0
i32.add)
;; Sub function
(func $sub (param i32 i32) (result i32)
local.get 0
local.get 1
i32.sub)
;; Exports
(export "add" (func $add))
(export "sub" (func $sub))
)
An example of a simple Wasm module in its text format (WAT). This module
defines and exports an add
and a sub
function.
In contrast to JavaScript, Wasm is not intended to be written directly (although it can be). Wasm is a compilation target; this means that source code from other programming languages is compiled to Wasm code using a compiler. The resulting Wasm code can then be executed on a Wasm runtime, for example, the browser.
This is one of the most powerful aspects of Wasm - it provides a language agnostic way to run code written in almost any programming language in a fast, platform-independent way.
A large number of languages today have production ready Wasm tooling.
Wasm code is packaged and distributed as .wasm
files called Wasm modules.
The following example demonstrates a simple WebAssembly module that imports a
console.log
function, and exports a single function (exportedFunc
). The
module is loaded and instantiated through the JavaScript code below, which
proceeds to execute the exported function.
(module
(func $log (import "console" "log") (param i32))
(func (export "exportedFunc")
i32.const 42
call $log))
A sample Wasm module with a single exported function, that logs 42 to the console when called.
const importObject = {
console: { log: (arg) => console.log(arg) },
};
WebAssembly.instantiateStreaming(
fetch("example.wasm"),
importObject
).then(
(obj) => obj.instance.exports.exportedFunc(),
);
JS code to instantiate and interact with a Wasm module.
The key high level elements of a Wasm module are summarised below.
High level structure of a Wasm module.
Memory, Code, Imports, and Exports form the essential components to understand Wasm’s anatomy from an architectural perspective.
On the browser, Wasm modules need to communicate with JavaScript code to provide receive useful input or provide output.
Generally speaking, Wasm functions can only accept and return simple numeric types (like integers and floats)6. To handle complex data (like strings or objects), both JavaScript and Wasm can use the shared linear memory to communicate.
For example, to pass a string from JavaScript to Wasm, you can:
Wasm code can then interpret these bytes into a readable string.
This mechanism requires that the host environment and the Wasm module both know how to correctly interpret the bytes in memory. Handling this manually can be complex, and so the necessary “glue” code is typically generated by tooling rather than written by hand.
Wasm virtual machines are 32 bit, which has implications for porting applications as we’ll see later in the series. This also means that Wasm modules can address up to 4GB (or 232 bytes) of linear memory. This is sufficient in most cases, but there are active proposals to support 64 bit and larger memory sizes7 for more demanding use-cases.
We will discuss further details about JS/Wasm interop and the Wasm memory model in a future post in the series.
Wasm’s design offers a few key benefits, as explicitly discussed in its design goals.
Wasm can execute at near native speed, enabling otherwise unachievable performance on client-side web applications. Its performance often surpasses JavaScript as a result of its design:
Wasm is not only fast, but also predictable:
Altogether, Wasm’s design makes it easier to write well performing code by default. For example, Figma saw a 3x improvement in load times of heavy files by switching to WebAssembly.
A Wasm module makes no assumptions about the platform on which it executes (including the browser). By design, its dependencies are made explicit in the form of imports. This makes Wasm modules executable on any spec-compliant Wasm runtime, provided the required imports can be supplied at runtime.
This design feature of Wasm has led to the development of many use-cases for Wasm outside the browser, despite browser based use-cases being its initial motivation. Wasm can now power not just browser applications, but also server-side runtimes, IoT devices and more.
Wasm code is executed in a memory-safe sandbox, isolating the running program from all other programs on the host system. By design, Wasm programs cannot interfere with the execution of, or memory from any other running code on the system, and can only access its well defined linear memory.
Wasm code can only escape the sandbox through imports explicitly supplied by the host, providing a clear access control mechanism for Wasm code. For browser use cases, this means that access to the DOM, any web APIs and the underlying system is only through explicitly provided JavaScript functions. In this way, Wasm code is subject to a permissions model that is as strict (or stricter) than JavaScript.
Wasm has matured into a foundational platform, on top of which a new generation of tooling, frameworks and application architectures is actively being developed, with a multitude of production ready apps being shipped today. It is transforming web development with its speed, portability, and safety.
In this introduction, we’ve explored Wasm’s history, core features, and benefits.In future articles, we will dive deeper into practical applications of Wasm, and specifically, practical ways to use Wasm to bring native applications to the web, with patterns and tips to help you succeed.
MS Edge developed a specialized pipeline for asm.js code. ↩
Optimizations were introduced in V8 specifically with asm.js in mind. ↩
A good explanation about why WebAssembly is faster than asm.js can be found here. ↩
Why create a new standard when there is already asm.js? Some commentary can be found here on the official WebAssembly website. ↩
Recent proposals extend Wasm’s primitive types new opaque “reference types” to allow passing more complex objects across the Wasm <-> host boundary, but the details of this are omitted for brevity. ↩
Memory64 is an active proposal to support 64 bit pointers and larger memory in Wasm. While implemented in some browsers, this is not stable at the time of writing. ↩
On this page