# WebAssembly tutorial

> Learn WebAssembly step by step by compiling a small module, loading it from JavaScript, and working with exports, imports, and memory.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-14 | Updated: 2026-08-15 | Topics: [JavaScript](https://flaviocopes.com/tags/js/) | Canonical: https://flaviocopes.com/webassembly/

WebAssembly is easier to understand when you run it.

So let's create a small module, compile it, load it in a browser, and call it from JavaScript.

The example will stay deliberately small. We are not building one particular application.

We are using a tiny module to learn the parts every WebAssembly program has:

- source code compiled to a `.wasm` binary
- exported functions the host can call
- imported functions supplied by the host
- linear memory shared with JavaScript
- a host environment that controls access to the outside world

By the end, you will know what WebAssembly is, how it works with JavaScript, and when it is worth using.

You only need basic JavaScript for this tutorial. If you need a refresher, my [free JavaScript course](https://flaviocopes.com/courses/javascript/) starts from the foundations.

## What WebAssembly is

**WebAssembly**, usually shortened to **Wasm**, is a portable binary instruction format.

Browsers can compile and execute that format inside a sandbox.

You normally do not write the binary by hand. You write Rust, C, C++, Go, AssemblyScript, or another supported language. A compiler turns that source into a `.wasm` file.

JavaScript then loads the file and talks to the module.

```text
Rust, C, C++, Go, ...
          |
          v
       compiler
          |
          v
      module.wasm
          |
          v
browser, Node.js, or another Wasm runtime
```

WebAssembly is not a programming language that replaces JavaScript.

It is a compilation target and execution format.

[JavaScript](https://flaviocopes.com/javascript/) remains excellent at user interfaces, browser APIs, events, networking, and application glue. Wasm is useful when we want portable compiled code or a fast CPU-heavy core.

The two are designed to work together.

## Start with the text format

A `.wasm` file is binary. Opening it in a text editor does not teach us much.

WebAssembly also has a readable text format called **WebAssembly Text**, or **WAT**.

WAT uses `.wat` files. Tools can convert WAT to Wasm and back again.

You will rarely write a large application in WAT. It is useful for learning, inspecting compiler output, creating tiny tests, and understanding the execution model.

That makes it perfect for this tutorial.

## Install the WebAssembly tools

We will use the [WebAssembly Binary Toolkit](https://github.com/WebAssembly/wabt), usually called WABT.

On macOS, install it with Homebrew:

```bash
brew install wabt
```

On Ubuntu or another Debian-based Linux distribution:

```bash
sudo apt install wabt
```

Check the compiler:

```bash
wat2wasm --version
```

WABT includes several small commands. We will use four:

- `wat2wasm` compiles text into a Wasm binary
- `wasm2wat` converts the binary back to readable text
- `wasm-objdump` shows the structure of a binary
- `wasm-validate` checks that a binary is a valid module

Create a directory for the tutorial:

```bash
mkdir webassembly-tutorial
cd webassembly-tutorial
```

## Write your first module

Create a file named `math.wat`:

```wasm
(module
  (func (export "add")
    (param $a i32)
    (param $b i32)
    (result i32)

    local.get $a
    local.get $b
    i32.add
  )
)
```

This module exports one function named `add`.

The function accepts two `i32` values and returns one `i32` value. An `i32` is a 32-bit integer.

The function body places both parameters on the Wasm value stack. `i32.add` removes those values, adds them, and places the result on the stack.

The last value becomes the function result.

Wasm is a stack machine. Instructions consume and produce values on an implicit stack.

For our function, the stack changes like this:

```text
[]
[a]
[a, b]
[a + b]
```

A compiler normally generates these instructions from a higher-level language.

## Compile WAT to Wasm

Compile the text file:

```bash
wat2wasm math.wat -o math.wasm
```

You now have two files:

```text
webassembly-tutorial/
├── math.wat
└── math.wasm
```

Check the binary:

```bash
wasm-validate math.wasm
```

No output means the module is valid.

Inspect its structure:

```bash
wasm-objdump -x math.wasm
```

The output shows a function section and an export named `add`.

You can also turn the binary back into text:

```bash
wasm2wat math.wasm
```

This round trip is useful when a toolchain gives you a `.wasm` file and you want to inspect what it contains.

## Load the module from JavaScript

Now create `index.html`:

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>WebAssembly tutorial</title>
  </head>
  <body>
    <p id="output">Loading...</p>

    <script type="module">
      const response = await fetch('./math.wasm')
      const bytes = await response.arrayBuffer()
      const { instance } = await WebAssembly.instantiate(bytes)

      const result = instance.exports.add(20, 22)

      document.querySelector('#output').textContent =
        `20 + 22 = ${result}`
    </script>
  </body>
</html>
```

Do not open the file directly from Finder.

The page uses `fetch()`, so serve the directory over HTTP:

```bash
python3 -m http.server 8000
```

Open `http://localhost:8000` in the browser.

You should see:

```text
20 + 22 = 42
```

We just completed the basic WebAssembly path:

1. `fetch()` downloaded the binary.
2. `arrayBuffer()` exposed its bytes.
3. `WebAssembly.instantiate()` compiled and instantiated the module.
4. `instance.exports.add` gave JavaScript access to the exported function.
5. JavaScript called that function like any other function.

The Wasm module did the addition. JavaScript loaded it and displayed the result.

## Compile while the file downloads

Browsers can compile a Wasm module while it streams over the network.

Replace the first three JavaScript lines with this:

```js
const { instance } = await WebAssembly.instantiateStreaming(
  fetch('./math.wasm'),
)
```

The rest of the code stays the same.

`instantiateStreaming()` avoids waiting for the complete file before compilation starts.

The server must send `.wasm` files with the `application/wasm` content type. If you cannot control the content type, use the `arrayBuffer()` version instead.

The local Python server normally sends the correct type.

## Understand modules and instances

The JavaScript API separates a **module** from an **instance**.

A `WebAssembly.Module` contains compiled code. A `WebAssembly.Instance` is a live copy of that module with its imports, memory, and state.

`WebAssembly.instantiate()` does both steps for us and returns the instance.

You can separate them when you want several instances from one compiled module:

```js
const response = await fetch('./math.wasm')
const bytes = await response.arrayBuffer()
const module = await WebAssembly.compile(bytes)

const first = await WebAssembly.instantiate(module)
const second = await WebAssembly.instantiate(module)

console.log(first.exports.add(2, 3)) //5
console.log(second.exports.add(10, 20)) //30
```

The browser compiles the bytes once. Each call to `instantiate()` creates another live instance.

Most applications only need one instance. The distinction matters when you build plugin systems, isolated workers, or several copies of a stateful module.

## Export memory from Wasm

Numbers cross the JavaScript-Wasm boundary directly.

Strings, arrays, images, and other larger values need memory.

Wasm uses a contiguous byte buffer called **linear memory**. JavaScript can access the same buffer through typed arrays.

Let's add memory to our module.

Replace `math.wat` with this:

```wasm
(module
  (memory (export "memory") 1)

  (data (i32.const 0) "Hello from WebAssembly")

  (func (export "add")
    (param $a i32)
    (param $b i32)
    (result i32)

    local.get $a
    local.get $b
    i32.add
  )
)
```

The module now exports one page of memory.

One WebAssembly memory page is 64 KiB. Our text begins at byte offset `0`.

Compile the module again:

```bash
wat2wasm math.wat -o math.wasm
```

Now read the bytes from JavaScript:

```js
const { instance } = await WebAssembly.instantiateStreaming(
  fetch('./math.wasm'),
)

const bytes = new Uint8Array(instance.exports.memory.buffer, 0, 22)
const message = new TextDecoder().decode(bytes)

console.log(message) //Hello from WebAssembly
```

`instance.exports.memory.buffer` is an `ArrayBuffer`.

`Uint8Array` gives JavaScript a byte-level view. `TextDecoder` converts those UTF-8 bytes into a JavaScript string.

My [ArrayBuffer guide](https://flaviocopes.com/arraybuffer/) explains this JavaScript binary-data layer in more detail.

The number `22` is the length of the stored message.

Real toolchains generate helpers that pass strings and objects through memory. Underneath those helpers, the same rules apply: one side writes bytes, passes a pointer and length, and the other side reads them.

Memory is where much of the integration work happens.

## Import a JavaScript function

Exports let JavaScript call Wasm.

Imports let Wasm call a function supplied by its host.

Add this import near the top of `math.wat`, after `(module`:

```wasm
(import "host" "log" (func $log (param i32)))
```

Then add an exported function that calls it:

```wasm
(func (export "run")
  i32.const 42
  call $log
)
```

Compile the module again:

```bash
wat2wasm math.wat -o math.wasm
```

The module now requires an import called `host.log`.

Provide it when you instantiate the module:

```js
const imports = {
  host: {
    log(value) {
      console.log(`Wasm sent ${value}`)
    },
  },
}

const { instance } = await WebAssembly.instantiateStreaming(
  fetch('./math.wasm'),
  imports,
)

instance.exports.run()
```

The console prints:

```text
Wasm sent 42
```

The two strings in WAT define the shape of the import object:

```text
"host" "log"
   |      |
   v      v
imports.host.log
```

Instantiation fails with a `WebAssembly.LinkError` when a required import is missing or has the wrong type.

This import model is also the security boundary.

The module cannot call `fetch()`, read the DOM, open a file, or access a database unless the host provides that capability.

At this point, the complete `math.wat` file contains:

```wasm
(module
  (import "host" "log" (func $log (param i32)))

  (memory (export "memory") 1)

  (data (i32.const 0) "Hello from WebAssembly")

  (func (export "add")
    (param $a i32)
    (param $b i32)
    (result i32)

    local.get $a
    local.get $b
    i32.add
  )

  (func (export "run")
    i32.const 42
    call $log
  )
)
```

This is the final module used by the remaining examples.

## Wasm does not access the DOM directly

A browser Wasm module does not know what `document`, `window`, or a button is.

JavaScript owns those APIs.

If a module needs to update the page, it calls an imported JavaScript function. Or it returns data and lets JavaScript update the page.

For example:

```js
const imports = {
  ui: {
    showResult(value) {
      document.querySelector('#output').textContent = value
    },
  },
}
```

This is a good separation.

Keep browser interactions in JavaScript. Put computation and portable compiled logic in Wasm.

Frameworks and language toolchains hide much of this bridge, but they cannot remove it.

## Use WebAssembly from Node.js

WebAssembly is not limited to browsers.

[Node.js](https://flaviocopes.com/nodejs/) exposes the same core `WebAssembly` JavaScript API.

Create `run.mjs` next to `math.wasm`:

```js
import { readFile } from 'node:fs/promises'

const bytes = await readFile(new URL('./math.wasm', import.meta.url))

const imports = {
  host: {
    log(value) {
      console.log(`Wasm sent ${value}`)
    },
  },
}

const { instance } = await WebAssembly.instantiate(bytes, imports)

console.log(instance.exports.add(7, 8)) //15
instance.exports.run()
```

Run it:

```bash
node run.mjs
```

Node reads the binary from disk instead of fetching it over HTTP.

The module itself does not change. The host around it changes.

## WAT is not the normal production workflow

We wrote WAT to see the machinery clearly.

For real projects, use a language and toolchain that targets Wasm.

C and C++ projects commonly use [Emscripten](https://emscripten.org/). It can compile existing code and generate the JavaScript glue needed for browser APIs.

Rust projects commonly use [`wasm-bindgen`](https://rustwasm.github.io/docs/wasm-bindgen/) and [`wasm-pack`](https://rustwasm.github.io/docs/wasm-pack/). They generate bindings for strings, objects, promises, and browser APIs.

AssemblyScript looks similar to TypeScript, but it is a separate language with types and restrictions chosen for WebAssembly compilation.

Go can compile to Wasm too. TinyGo often produces smaller modules for focused Wasm work.

The exact toolchain changes. The module model does not:

- the compiler produces a Wasm module
- the module exports functions, memory, tables, or globals
- the host supplies required imports
- values cross the boundary through supported types or shared memory

Learning that model first makes every toolchain easier to understand.

## WebAssembly is not automatically faster

Wasm is compact, predictable, and designed for efficient compilation.

That does not mean every Wasm function beats JavaScript.

Modern JavaScript engines optimize hot code aggressively. A small calculation can be just as fast in JavaScript, and sometimes faster.

Crossing between JavaScript and Wasm also has a cost. Calling a Wasm function once per pixel or once per small object can lose the benefit you expected.

The best pattern is to move a meaningful block of work across the boundary:

```text
JavaScript prepares input
          |
          v
one Wasm call performs substantial work
          |
          v
JavaScript receives the result
```

Do not rewrite code because Wasm sounds fast.

Profile the application first. Find a CPU-heavy section. Measure the JavaScript version, the Wasm version, and the cost of moving data between them.

## Where WebAssembly fits

WebAssembly is a strong fit for work such as:

- image, audio, and video processing
- compression and decompression
- parsers and compilers
- cryptographic operations
- physics and simulation engines
- emulators
- database engines
- existing Rust, C, or C++ libraries brought to the web

It is a poor fit for ordinary DOM manipulation, form handling, network orchestration, and small pieces of business logic.

JavaScript already handles those jobs well.

I would start with JavaScript. If profiling found a real CPU bottleneck, I would isolate that work behind a small interface and test a Wasm implementation.

I would also use Wasm when a mature library already exists in another language. Reusing proven code can matter more than raw speed.

## The browser sandbox

WebAssembly runs inside the browser's security model.

The module gets its own linear memory. Memory accesses are bounds-checked. Code cannot jump to arbitrary addresses outside valid Wasm functions.

The module receives capabilities through imports.

This means a Wasm module does not automatically get filesystem, network, camera, microphone, or DOM access.

But sandboxed does not mean trustworthy.

A malicious module can still consume CPU, allocate memory, exploit a bug in an imported host function, or process data incorrectly. Treat third-party Wasm like any other third-party executable dependency.

Validate where it came from, keep its permissions narrow, and update it when security fixes ship.

## Wasm outside the browser and WASI

The core WebAssembly specification defines computation. It does not define files, sockets, clocks, or random numbers.

Browsers provide capabilities through JavaScript imports.

Outside the browser, runtimes often use **WASI**, the WebAssembly System Interface.

WASI defines portable interfaces for system capabilities. A runtime such as Wasmtime can decide which files, network connections, clocks, or other resources a component may use.

WASI 0.3 added native async functions, streams, and futures to the Component Model in 2026.

This does not mean one Wasm binary runs unchanged everywhere.

A browser module, a WASI module, and a Component Model component can expect different imports and interfaces. Check what the target runtime supports before choosing a toolchain target.

The official [WASI documentation](https://wasi.dev/) explains the current releases and runtime support.

## WebAssembly today

WebAssembly is an open web standard supported by modern browsers and major server runtimes.

Wasm 3.0 became the live standard in September 2025. It added features including 64-bit address spaces, garbage-collected types, and native exception handling.

Those features make Wasm a better target for more languages and larger programs.

They do not change the foundation we used in this tutorial.

A module still declares imports and exports. The host still instantiates it. JavaScript still communicates through functions and memory.

That small model scales from our `add` function to design tools, media editors, game engines, database libraries, and server components.

The official [WebAssembly site](https://webassembly.org/) contains the specifications, feature status, and more learning material.
