Run WebAssembly in a single HTML file

A WebAssembly module is a small binary file. Put its bytes inside your HTML, hand them to WebAssembly.instantiate(), and the page runs it without a server.

WebAssembly (Wasm) is a compact binary format that browsers run alongside JavaScript. A C, C++, Rust or hand-written module compiles to a .wasm file, and JavaScript loads it and calls the functions it exports.

Most guides load that file with fetch(), which needs a server. For one HTML file, skip the fetch: put the module's bytes in the page and pass them to WebAssembly.instantiate().

Try it. Change either number. The sum is computed by a 41-byte WebAssembly module written out inside the page.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebAssembly in one HTML file</title>
<style>
  body { margin: 0; padding: 20px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .box { max-width: 420px; background: #fff; border-radius: 12px; padding: 18px; box-shadow: 0 4px 16px rgba(0, 0, 0, .08); }
  .row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 20px; }
  input { width: 90px; font: inherit; padding: 6px 8px; border: 1px solid #cfd4dc; border-radius: 8px; }
  #out { font-weight: 700; color: #0f5132; }
  p { font-size: 14px; color: #4b5563; margin: 14px 0 0; line-height: 1.5; }
  code { background: #eef1f5; border-radius: 4px; padding: 0 3px; }
</style>
</head>
<body>
<div class="box">
  <div class="row">
    <input id="a" type="number" value="20"> + <input id="b" type="number" value="22"> = <span id="out">…</span>
  </div>
  <p id="note">Loading the module…</p>
</div>

<script>
  // The whole .wasm file, 41 bytes. It exports one function: add(i32, i32) -> i32
  const bytes = new Uint8Array([
    0,97,115,109, 1,0,0,0,              // "\0asm", version 1
    1,7,1,96,2,127,127,1,127,           // type: (i32, i32) -> i32
    3,2,1,0,                            // one function of that type
    7,7,1,3,97,100,100,0,0,             // export it as "add"
    10,9,1,7,0,32,0,32,1,106,11         // body: local.get 0, local.get 1, i32.add
  ]);

  const a = document.getElementById('a');
  const b = document.getElementById('b');
  const out = document.getElementById('out');

  WebAssembly.instantiate(bytes).then(({ instance }) => {
    const add = instance.exports.add;  // a normal JavaScript function now
    const update = () => { out.textContent = add(Number(a.value), Number(b.value)); };
    a.addEventListener('input', update);
    b.addEventListener('input', update);
    update();
    document.getElementById('note').innerHTML =
      'The sum comes from <code>instance.exports.add</code>, compiled from ' + bytes.length + ' bytes in this page.';
  });
</script>
</body>
</html>
A complete .wasm file as 41 numbers in a Uint8Array. Edit the code and the example reruns.

The add function on instance.exports behaves like any JavaScript function. The page has no other files and makes no requests.

What WebAssembly is, in one paragraph

MDN describes WebAssembly as a low-level, assembly-like language with a compact binary format. Its job is to be a compilation target: code written in C/C++, C# or Rust is compiled to it and runs in the browser.

It does not replace JavaScript. It has no access to the page by itself. JavaScript loads the module, gives it any functions it needs, and calls what it exports.

From source code to a function JavaScript can call. Only the last two steps run in the page.
From source code to a function JavaScript can call. Only the last two steps run in the page.

The first two steps happen once, with a compiler. The last two run every time the page opens, and that part fits in one HTML file.

The smallest working example

Every .wasm file starts with the same eight bytes: \0asm and the version number 1. The specification calls these the magic number and the version. The rest describes functions, exports and code.

The text form of the module in the first example looks like this. It is the same add function MDN uses to explain the text format:

(module
  (func (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))

A tool such as wat2wasm from wabt turns that into 41 bytes. The page needs only those bytes and three lines of JavaScript:

const bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0 /* ... */]);
const { instance } = await WebAssembly.instantiate(bytes);
console.log(instance.exports.add(20, 22)); // 42

WebAssembly.instantiate() returns a promise. Called with bytes, it resolves to an object holding the compiled module and the instance with its exports.

Why fetching a .wasm file fails in one HTML file

The usual tutorial code loads the module from its own file:

WebAssembly.instantiateStreaming(fetch('add.wasm'));

MDN calls this the quickest, most efficient way, and on a server it is. In a single-file page it fails.

A separate .wasm file needs a request. Bytes inside the page need none.
A separate .wasm file needs a request. Bytes inside the page need none.
  • Opened from disk, fetch() of a local file is refused. Chrome reports Failed to fetch, Firefox a NetworkError, and WebKit, the engine behind Safari, Load failed. The file:// protocol explains why.
  • Served, but with the wrong type, instantiateStreaming() rejects with a TypeError unless the response is application/wasm.
  • Sent as one file, the .wasm simply is not there. The page arrives alone.

Embedding the bytes removes the request, so none of these can happen.

Passing arrays and text: imports and memory

WebAssembly functions take and return only numbers: i32, i64, f32 and f64. To hand over a list or a string, write it into the module's memory and pass its position and length.

This module needs one thing from JavaScript: an env.log function that it calls with each running total. It exports sum(ptr, len) and its memory.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebAssembly imports and memory</title>
<style>
  body { margin: 0; padding: 18px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .box { max-width: 520px; background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 4px 16px rgba(0, 0, 0, .08); }
  label { font-size: 14px; color: #4b5563; }
  input { width: 100%; box-sizing: border-box; font: 16px system-ui, sans-serif; padding: 8px; margin: 6px 0 12px; border: 1px solid #cfd4dc; border-radius: 8px; }
  button { font: 600 14px system-ui, sans-serif; padding: 9px 12px; border-radius: 8px; border: 0; cursor: pointer; margin: 0 6px 6px 0; }
  #run { background: #16a34a; color: #fff; }
  #broken { background: #fde2da; color: #9a3412; }
  pre { background: #1d2330; color: #e5e7eb; border-radius: 8px; padding: 10px; font-size: 13px; min-height: 120px; white-space: pre-wrap; word-break: break-word; margin: 8px 0 0; }
  .err { color: #fca5a5; }
</style>
</head>
<body>
<div class="box">
  <label for="nums">Numbers to add up (whole numbers, comma separated)</label>
  <input id="nums" value="5, 10, 20, 7">
  <button id="run">Sum in WebAssembly</button>
  <button id="broken">Load without the import</button>
  <pre id="log">Press a button.</pre>
</div>

<script>
  // A compiled module pasted as base64 (112 bytes).
  // It imports env.log, exports its memory and sum(ptr, len).
  const wasmBase64 = 'AGFzbQEAAAABCwJgAX8AYAJ/fwF/AgsBA2VudgNsb2cAAAMCAQEFAwEAAQcQAgZtZW1vcnkCAANzdW0AAQoxAS8BAX8CQANAIAFFDQEgAiAAKAIAaiECIAIQACAAQQRqIQAgAUEBayEBDAALCyACCw==';
  const bytes = Uint8Array.from(atob(wasmBase64), (c) => c.charCodeAt(0));

  const log = document.getElementById('log');
  const show = (text, cls) => {
    const line = document.createElement('div');
    line.textContent = text;
    if (cls) line.className = cls;
    log.append(line);
  };

  document.getElementById('run').addEventListener('click', async () => {
    log.textContent = '';
    // Every import the module declares must be supplied here
    const imports = { env: { log: (n) => show('running total: ' + n) } };
    const { instance } = await WebAssembly.instantiate(bytes, imports);
    const { memory, sum } = instance.exports;

    // Copy the numbers into wasm memory, 4 bytes each, starting at byte 0
    const nums = document.getElementById('nums').value.split(',').map(Number).filter(Number.isFinite);
    new Int32Array(memory.buffer, 0, nums.length).set(nums);

    // Pass only where the data is and how long it is
    show('sum(0, ' + nums.length + ') = ' + sum(0, nums.length));
  });

  document.getElementById('broken').addEventListener('click', async () => {
    log.textContent = '';
    try {
      await WebAssembly.instantiate(bytes);  // no import object
    } catch (err) {
      show(err.name + ': ' + err.message, 'err');
    }
  });
</script>
</body>
</html>
JavaScript writes the numbers into wasm memory, then calls sum(0, count). The red button skips the import object.
Imports go in, exports come out, and arrays travel through shared memory.
Imports go in, exports come out, and arrays travel through shared memory.

The key lines are short:

const imports = { env: { log: (n) => console.log(n) } };
const { instance } = await WebAssembly.instantiate(bytes, imports);
const { memory, sum } = instance.exports;
new Int32Array(memory.buffer, 0, 4).set([5, 10, 20, 7]);
sum(0, 4); // 42

The module is stored as a base64 string this time and decoded with atob(). That is the form most people paste, because it is shorter than a list of numbers.

Press the red button. Without the import object, the browser refuses to create the instance. MDN states the rule: every import the module declares needs a matching property, or a LinkError is thrown.

Number rules that surprise people

Exported functions convert their arguments. For an i32 parameter, the test results were:

You pass The function receives Why
2.7 2 The fraction is dropped
'20' 20 The string is converted to a number
'x' 0 Not a number becomes 0
2147483647 + 1 -2147483648 i32 wraps around at 32 bits
An i64 result A BigInt A plain number cannot hold every 64-bit value

Convert input values with Number() yourself, as the first example does, so the conversion is visible in your code.

A finished example: a WebAssembly text playground

You do not need a compiler on your computer to try your own module. wabt.js is the WebAssembly Binary Toolkit built for the browser. Loaded from jsDelivr with a pinned version, it turns WebAssembly text into .wasm bytes inside the page.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebAssembly text playground</title>
<!-- wabt.js: the WebAssembly Binary Toolkit compiled for the browser -->
<script src="https://cdn.jsdelivr.net/npm/wabt@1.0.39/index.js"></script>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .box { max-width: 640px; background: #fff; border-radius: 12px; padding: 14px; box-shadow: 0 4px 16px rgba(0, 0, 0, .08); }
  textarea { width: 100%; box-sizing: border-box; height: 190px; font: 13px/1.45 ui-monospace, Consolas, monospace; padding: 10px; border: 1px solid #cfd4dc; border-radius: 8px; resize: vertical; }
  .bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 10px 0; font-size: 14px; }
  input { width: 70px; font: 15px system-ui, sans-serif; padding: 6px; border: 1px solid #cfd4dc; border-radius: 8px; }
  button { font: 600 14px system-ui, sans-serif; padding: 8px 12px; border: 0; border-radius: 8px; cursor: pointer; background: #16a34a; color: #fff; }
  button:disabled { background: #9ca3af; cursor: wait; }
  select { font: 14px system-ui, sans-serif; padding: 6px; border-radius: 8px; border: 1px solid #cfd4dc; }
  #out { background: #1d2330; color: #e5e7eb; border-radius: 8px; padding: 10px; font: 13px/1.45 ui-monospace, Consolas, monospace; white-space: pre-wrap; word-break: break-all; min-height: 140px; margin: 0; }
  .ok { color: #86efac; } .err { color: #fca5a5; } .dim { color: #9ca3af; }
</style>
</head>
<body>
<div class="box">
  <div class="bar">
    <label>Example <select id="pick">
      <option value="fac">factorial</option>
      <option value="broken">typo (compile error)</option>
    </select></label>
  </div>
  <textarea id="src" spellcheck="false"></textarea>
  <div class="bar">
    <label>main( <input id="arg" type="number" value="10"> )</label>
    <button id="run" disabled>Loading wabt…</button>
  </div>
  <pre id="out"><span class="dim">Write WebAssembly text, then compile and run it.</span></pre>
</div>

<script>
  const samples = {
    fac: `(module
  ;; main(n) = 1 * 2 * ... * n, with 64-bit integers
  (func (export "main") (param $n i32) (result i64)
    (local $acc i64)
    (local.set $acc (i64.const 1))
    (block $done
      (loop $next
        (br_if $done (i32.le_s (local.get $n) (i32.const 1)))
        (local.set $acc (i64.mul (local.get $acc) (i64.extend_i32_s (local.get $n))))
        (local.set $n (i32.sub (local.get $n) (i32.const 1)))
        (br $next)))
    (local.get $acc)))`,
    broken: `(module
  (func (export "main") (param $n i32) (result i32)
    (i32.ad (local.get $n) (i32.const 1))))`
  };

  const src = document.getElementById('src');
  const out = document.getElementById('out');
  const run = document.getElementById('run');
  const pick = document.getElementById('pick');
  const print = (text, cls) => {
    const span = document.createElement('span');
    span.textContent = text + '\n';
    if (cls) span.className = cls;
    out.append(span);
  };

  src.value = samples.fac;
  pick.addEventListener('change', () => { src.value = samples[pick.value]; });

  WabtModule().then((wabt) => {
    run.disabled = false;
    run.textContent = 'Compile and run';

    run.addEventListener('click', async () => {
      out.textContent = '';
      let bytes;
      try {
        // Text format -> binary .wasm bytes
        const mod = wabt.parseWat('main.wat', src.value);
        bytes = mod.toBinary({}).buffer;
        mod.destroy();
      } catch (err) {
        print('Compile error:\n' + err.message, 'err');
        return;
      }
      print(bytes.length + ' bytes of .wasm', 'dim');
      try {
        const { instance } = await WebAssembly.instantiate(bytes);
        const n = Number(document.getElementById('arg').value);
        print('main(' + n + ') = ' + instance.exports.main(n), 'ok');
      } catch (err) {
        print(err.name + ': ' + err.message, 'err');
      }
      // Base64 you can paste into another page and decode with atob()
      let bin = '';
      bytes.forEach((byte) => { bin += String.fromCharCode(byte); });
      print('\nbase64:\n' + btoa(bin), 'dim');
    });
  });
</script>
</body>
</html>
wabt.js compiles the text, the browser runs it, and the base64 output can be pasted into your own page.
<script src="https://cdn.jsdelivr.net/npm/wabt@1.0.39/index.js"></script>

The script defines a global WabtModule(). Its promise gives you parseWat(), which reads the text, and toBinary(), which returns the bytes. Pick the typo example to see the compile error with the line and column.

The base64 under the result is the step that links this playground to the earlier examples. Copy it into a page, decode it with atob(), and that page runs the module without loading wabt.js.

Bigger programs: C, C++ and Rust

For real code you compile on your own computer. Emscripten is a compiler toolchain to WebAssembly. Run with -o hello.js, it writes two files, hello.js and hello.wasm. Its SINGLE_FILE setting puts the wasm inside the JavaScript file instead:

emcc hello.c -o hello.js -sSINGLE_FILE

Paste the resulting hello.js into a <script> tag and the page needs no separate .wasm. This build runs on your machine, not in the example frames above, so treat it as a recipe rather than something tested here.

Large modules become large pages. If the module does heavy work, run it inside a Web Worker so the page stays responsive.

When it does not work

What you see Cause Fix
TypeError: Failed to fetch The page fetches a .wasm file from disk Embed the bytes in the page
Incorrect response MIME type The server does not send application/wasm Embed the bytes, or fix the server's type
CompileError about the magic word The bytes are not a wasm file, often an HTML error page Check the base64 or the array
LinkError on instantiate An import is missing or has the wrong kind Supply every import the module lists
TypeError about the imports argument No import object passed, but the module has imports Pass the import object as the second argument
exports.add is not a function The export name differs Log Object.keys(instance.exports)
Wrong or negative results Number conversion or i32 wrap-around Use Number(), or i64 / f64

For script errors that are not about WebAssembly, see JavaScript not working in HTML.

A page that runs WebAssembly is hard to send as a file. An .html attachment may open as plain code on a phone, and a separate .wasm file is easy to lose on the way.

With the bytes embedded, everything is in one page. Paste it into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can use the module themselves, without an account. Scripts from cdn.jsdelivr.net load, so the playground works too. If you change the code later, the same link shows the new version.

Questions people ask

Do I need a web server to run WebAssembly?

Only if you load the .wasm as a separate file. fetch() does not work on pages opened from disk, and instantiateStreaming() needs the file served with the application/wasm MIME type. Bytes stored inside the page avoid both, so the page runs from a file or a share link.

Can I pass a string to a WebAssembly function?

Not directly. WebAssembly functions take and return numbers: i32, i64, f32 and f64. Text and arrays are written into the module's memory as bytes, and the function receives where they start and how long they are.

Can I use script type="module" or import to load a .wasm file?

Not in the way you import a JavaScript file. MDN notes that WebAssembly is not yet integrated with script type="module" or import statements, so pages load modules through the WebAssembly JavaScript API.

Why does a function that returns i64 give me a BigInt?

JavaScript's Number type cannot hold the full i64 range without losing precision, so i64 values cross into JavaScript as BigInt. In the playground, main(20) returns 2432902008176640000 without rounding. Use i32 or f64 if you want ordinary numbers.

Is base64 the only way to embed the bytes?

No. A Uint8Array literal works too, as in the first example. Base64 is shorter to paste, and atob() turns it back into bytes in one line. Any form works as long as WebAssembly.instantiate() receives the exact bytes of the .wasm file.

Keep reading