JavaScript & WebAssembly
Learn when WebAssembly helps, how JavaScript loads and calls it, how memory is shared, and how modern Wasm features fit together.
- 01Load a moduleCheck Wasm bytes, load a small module, and call its function from JavaScript.
- 02Explain how the two sides talkSay what imports, exports, and shared memory are, and why an old view has 0 bytes after the memory grows.
- 03Know when Wasm fitsSay when Wasm helps, what V8's two compilers do, and what WasmGC and JSPI add, without treating Wasm as a JavaScript replacement.
What WebAssembly is
WebAssembly, or Wasm for short, is a kind of program that browsers can run next to JavaScript. It is stored as binary: plain numbers (bytes), not text you can read.
People rarely write Wasm by hand. They write code in another language, such as C, C++, or Rust. Then a compiler, a tool that translates code, turns it into Wasm. JavaScript then loads the Wasm and calls it.
Wasm works with JavaScript. It does not replace it. It helps when a small part of an app does the same number work again and again. For example:
- resizing an image
- decoding video or sound
- running a game loop
- doing big calculations
Page layout, buttons, forms, and network requests stay in JavaScript. The browser's tools for these jobs are made for JavaScript.
A Wasm file is called a module. Before a module runs, the browser checks that its bytes are well formed. The module can only use what JavaScript hands it. The same module runs on any computer, and the browser still turns it into fast code for that computer.
Do not pick Wasm just because a job sounds hard. First profile the page: measure where the time really goes. Often the slow part is a page update or a wait for the network, and Wasm cannot speed those up. A short calculation that runs many times is a better fit.
Wasm is a binary program format that runs the same way on every computer. JavaScript loads a Wasm module and calls the functions the module offers. The two can also share one block of bytes, called linear memory.
The table below compares the two. Each row asks one question and answers it for JavaScript and for Wasm.
| Question | JavaScript | WebAssembly |
|---|---|---|
| What is it best at? | Using the browser's tools and running the app. | Doing one piece of number work fast. |
| How is it usually written? | People write and keep the code by hand. | A compiler makes it from another language. |
| What can it pass to the other side? | Objects, typed arrays, and numbers. | Numbers, and bytes in shared memory. |
| What should you ask first? | Which user action needs work? | Did measuring show a short job that repeats a lot? |
A home cook can make a whole meal. A food processor chops very fast, but only chops. The cook still plugs it in, puts the food in, and serves the result.
- In real life: The cook decides what to make
- In JavaScript: JavaScript runs the app
- In real life: The food processor does one job again and again
- In JavaScript: Wasm does one piece of number work
- In real life: The cook plugs it in and puts food in
- In JavaScript: JavaScript loads the module and calls it
- In real life: The cook serves the result
- In JavaScript: JavaScript uses the value Wasm returns
This is the last lesson in the runtime stage. The next stage starts with How to read ECMAScript. The earlier typed arrays lesson helps here. Typed arrays also show raw bytes as numbers, and Wasm uses them a lot.
A tiny Wasm module
The easiest first module holds one function that adds two whole numbers. The bytes below are a complete module. Here is what they hold, in order:
- a start mark,
\0asm, that says "this is Wasm", and a version number - the shape of the function: two numbers in, one number out
- one function with that shape
- an export: a public name,
add, that JavaScript can call - the steps that read the two numbers and add them
const bytes = new Uint8Array([0,97,115,109,1,0,0,0,1,7,1,96,2,127,127,1,127,3,2,1,0,7,7,1,3,97,100,100,0,0,10,9,1,7,0,32,0,32,1,106,11]);const { instance } = await WebAssembly.instantiate(bytes);console.log(instance.exports.add(2, 3));Uint8Array is a typed array: a list where each item is one byte, a whole number from 0 to 255.
- Line 1 puts the 41 module bytes in a
Uint8Array. - Line 2 calls
WebAssembly.instantiate. It checks the bytes, turns them into machine code, and makes an instance: a ready-to-use copy of the module. - Line 3 calls the exported
addfunction throughinstance.exports. It prints5.
add function with 2 and 3 gives 5.The first 8 bytes are the start mark and the version. Each part after that is called a section. A section starts with:
- an ID byte that says which kind of section it is
- a length that says how many bytes follow
Because of the length, the browser can jump over a section it does not need yet.
const bytes = new Uint8Array([0,97,115,109,1,0,0,0,1,7,1,96,2,127,127,1,127,3,2,1,0,7,7,1,3,97,100,100,0,0,10,9,1,7,0,32,0,32,1,106,11]);const sections = [ ["header and version", 0, 8], ["type", 8, 17], ["function", 17, 21], ["export", 21, 30], ["code", 30, 41],]; for (const [name, start, end] of sections) { console.log(name, bytes.slice(start, end).length);}Line 1 holds the same 41 bytes. Lines 3 to 7 give each part a name, a start position, and an end position:
- header and version
- type: the function's shape
- function: which shape the function uses
- export: the public name
- code: the steps
The loop on lines 10 to 12 prints each name and its size. It prints header and version 8 first and code 11 last. These names only fit this one module. The code is not a tool that can read any Wasm file.
You will almost never write these bytes yourself. A compiler writes them. People can read Wasm in a text form called WAT (WebAssembly Text). Below is the same add function in WAT:
i32means a 32-bit whole number.local.get 0andlocal.get 1read the two inputs.i32.addadds them.- The last line exports the function under the name
add.
(module
(func $add (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
(export "add" (func $add)))This replays the lesson's real call to the add module. It is not a tool that looks inside the engine.
script
const { instance } = await WebAssembly.instantiate(bytes);console.log(instance.exports.add(2, 3));This walks through the parts of this one add module. It is not a tool that can read any Wasm file.
script
const bytes = new Uint8Array([0,97,115,109,1,0,0,0,1,7,1,96,2,127,127,1,127,3,2,1,0,7,7,1,3,97,100,100,0,0,10,9,1,7,0,32,0,32,1,106,11]);const sections = [ ["type", 8, 17], ["function", 17, 21], ["export", 21, 30], ["code", 30, 41],]; for (const [name, start, end] of sections) { console.log(name, bytes.slice(start, end).length);}Loading and calling modules
Three words come up again and again when you load Wasm:
- A module is the Wasm code, checked and turned into machine code. On its own, it cannot run yet.
- An instance is a module that is ready to run. It has its own memory and the extra things JavaScript handed it.
- Instantiation is the step that makes an instance from a module.
An instance offers its exports to JavaScript. Exports can be:
- functions
- memories (blocks of bytes)
- tables: lists that hold things such as functions, not bytes
- globals: single values
Wasm functions only take and return a few kinds of values:
i32andi64are whole numbers (32 and 64 bits).f32andf64are numbers with decimals.- Reference types point at things, such as functions, instead of holding bytes.
When an i64 value goes to or from JavaScript, it is a BigInt, not a normal number.
If you get bytes from somewhere you do not trust, check them first. WebAssembly.validate answers true or false: are these bytes a well-formed Wasm module?
const bytes = new Uint8Array([0,97,115,109,1,0,0,0,1,7,1,96,2,127,127,1,127,3,2,1,0,7,7,1,3,97,100,100,0,0,10,9,1,7,0,32,0,32,1,106,11]);const broken = new Uint8Array([0, 97, 115, 109]);console.log(WebAssembly.validate(bytes));console.log(WebAssembly.validate(broken));- Line 1 holds the good add module.
- Line 2 holds only the first 4 bytes, the start mark.
- Line 3 prints
true. - Line 4 prints
false.
validate does not run the module. It also cannot tell you if the module does anything useful. It only checks the format.
The table below lists the ways to load Wasm. Each row is one tool: what it does, and when to use it.
| When you write | What it does | When to use it |
|---|---|---|
WebAssembly.validate(bytes) | Checks that the bytes are a well-formed module. Answers true or false. | Before you load bytes you do not trust. It does not make an instance. |
WebAssembly.instantiate(bytes) | Turns the bytes into machine code and makes an instance. | When you already have the bytes, for example in a Uint8Array. |
WebAssembly.instantiateStreaming(fetch(url)) | Starts making the instance while the file is still downloading. | When a server sends a .wasm file labelled application/wasm. |
instance.exports.add(2, 3) | Calls a function that the module offers to JavaScript. | After the instance is ready, to use the module. |
Wasm usually comes from a file on a server. With streaming, the browser starts turning the file into machine code while it is still downloading. For this to work, the server must label the file as application/wasm. That label is called the MIME type. This example needs a real file, so you cannot run it here.
const { instance } = await WebAssembly.instantiateStreaming(
fetch("add.wasm"),
);
console.log(instance.exports.add(2, 3));- Lines 1 and 2 start a download with
fetchand pass it toinstantiateStreaming.awaitwaits until the instance is ready. - Line 4 calls the exported
addfunction.
Things can also go the other way. JavaScript can hand functions or memory to the module. These are called imports. You pass them as the second argument to instantiate. Then Wasm code can call those JavaScript functions.
Liftoff and TurboFan: two compilers
Wasm bytes are not the machine code of your computer. The browser must turn them into machine code first. This lesson looks at V8, the engine inside Chrome and Node.js. V8 has two compilers for Wasm:
- Liftoff makes machine code very quickly, so the app can start soon. Its code is simple and not the fastest.
- TurboFan takes more time, but makes faster machine code.
Using a quick compiler first and a better one later is called tiered compilation. Each compiler is one tier. The code below is a small make-believe model of that choice.
function chooseTier(calls) { return calls >= 3 ? "TurboFan" : "Liftoff";} console.log(chooseTier(1));console.log(chooseTier(3));- Line 1 takes
calls: how many times a function has been called. - Line 2 answers
"Liftoff"for fewer than 3 calls and"TurboFan"for 3 or more. - Lines 5 and 6 print
Liftoffand thenTurboFan.
The number 3 is made up for the lesson. Real engines use their own rules to decide when to switch.
function describeTier(calls) { if (calls === 0) return "not called"; if (calls < 3) return "Liftoff baseline"; return "TurboFan candidate";} console.log(describeTier(0));console.log(describeTier(1));console.log(describeTier(3));- Line 2 says
"not called"when the function has not run yet. - Line 3 says
"Liftoff baseline"for the first few calls. A baseline is the simple first version of the code. - Line 4 says
"TurboFan candidate"for later calls. A candidate is a function that may get the faster code. - Lines 7 to 9 print the three steps in order.
Here is what V8's own documents say happens:
- When a Wasm function is first called, Liftoff compiles it.
- If the function runs a lot (it is hot), TurboFan compiles it again in the background.
- TurboFan works harder. For example, it keeps values in the fast spots of the processor, removes repeated work, and copies small functions into the places that call them.
- A call that is already running on Liftoff code finishes on Liftoff code. Later calls can use the TurboFan code.
The tiered compilation lesson shows the same idea for JavaScript code.
Liftoff helps the app start fast. TurboFan helps busy code run fast. The app author does not change anything: the switch happens by itself.
Sharing linear memory
Linear memory is one long row of bytes, numbered from 0. Wasm keeps its data there. There are two ways to set it up:
- JavaScript makes the memory and hands it to the module.
- The module makes the memory and exports it.
Either way, JavaScript reads and writes the bytes through a view: a typed array such as Uint8Array laid over the memory. Both sides see the very same bytes. Nothing is copied.
Memory comes in pages. One page is 65536 bytes (64 KiB). memory.buffer is the block of bytes, and byteLength is its size in bytes.
const memory = new WebAssembly.Memory({ initial: 1 });const oldView = new Uint8Array(memory.buffer);oldView[12] = 42;console.log(memory.buffer.byteLength, oldView[12]);memory.grow(1);console.log(memory.buffer.byteLength, oldView.byteLength);- Line 1 makes a memory with one page.
- Line 2 makes a view,
oldView, over that memory. - Line 3 writes
42into byte number 12. - Line 4 prints
65536 42: one page, and the byte we wrote. - Line 5 grows the memory by one page with
grow(1). - Line 6 prints
131072 0. The memory now has two pages. ButoldViewnow has 0 bytes.
Why 0? Growing swaps in a new, bigger block of bytes. The old view still points at the old block, which is now empty and unusable. We say the old view is detached.
grow.So after grow, always make a fresh view from memory.buffer.
Some numbers take more than one byte. The order of those bytes matters. Wasm memory always stores the small end first: the low byte comes before the high byte. This order is called little-endian. A DataView is a view that reads and writes numbers of any size. You pass true to tell it to use little-endian order.
const memory = new WebAssembly.Memory({ initial: 1 });const view = new DataView(memory.buffer); view.setUint16(0, 500, true);console.log(view.getUint8(0), view.getUint8(1));console.log(view.getUint16(0, true));- Line 1 makes the memory.
- Line 2 makes a
DataViewover it. - Line 4 writes
500into bytes 0 and 1, low byte first. - Line 5 reads each byte alone and prints
244 1. - Line 6 reads both bytes as one number and prints
500.
Why 244 and 1? 500 is 1 × 256 + 244. So the low byte is 244 and the high byte is 1.
Wasm can also run on several threads at once. Then the memory must be shared, using a SharedArrayBuffer, and the threads must take turns with atomics (safe, one-at-a-time operations). On the web, a page only gets SharedArrayBuffer when it is cross-origin isolated, a special security setting. The editor on this page does not have that setting.
Two cooks share one cutting board. They do not pass food back and forth. Each one just reaches for the same board.
- In real life: Two cooks use one cutting board
- In JavaScript: JavaScript and Wasm use one memory
- In real life: A cook puts a piece at one spot on the board
- In JavaScript: A view writes a byte at one position
- In real life: Both cooks see the same piece
- In JavaScript: Both sides read the same bytes
- In real life: A bigger board takes the old board's place
- In JavaScript: Growing memory leaves old views with 0 bytes
This replays the lesson's real memory code. It shows what JavaScript can see, not how the engine stores the bytes inside.
script
const oldView = new Uint8Array(memory.buffer);oldView[12] = 42;console.log(memory.buffer.byteLength, oldView[12]);memory.grow(1);console.log(memory.buffer.byteLength, oldView.byteLength);const memory = new WebAssembly.Memory({ initial: 1 });const oldView = new Uint8Array(memory.buffer);oldView[12] = 42;console.log(memory.buffer.byteLength, oldView[12]);memory.grow(1);console.log(memory.buffer.byteLength, oldView.byteLength);42JavaScript wrote this byte through a Uint8Array view.
131072One more page adds 65536 bytes.
0The old view has 0 bytes. Make a new view after grow.
Byte 12 now holds 42. One page is 65536 bytes. After grow, the memory has 131072 bytes, and the old view has 0 bytes.
WasmGC: the browser cleans up Wasm objects
Some languages, such as Java, Kotlin, and Dart, use a garbage collector. That is a helper that finds objects nobody uses any more and frees their memory. JavaScript has one too.
Plain Wasm only has linear memory: bytes, with no idea of objects. So these languages used to pack their own garbage collector into every Wasm file. That makes the file bigger.
WasmGC fixes this. It adds two kinds of objects to Wasm:
- structs: objects with a fixed set of fields
- arrays: lists of values
The browser's own garbage collector looks after these objects. So the language no longer needs to ship its own. The files get smaller, and the browser knows the shape of each object.
You do not use WasmGC in your JavaScript code. It is something the compiler for a language uses when it makes Wasm. The language needs a compiler that knows how to write WasmGC.
V8 says WasmGC shipped in Chrome 119. Other browsers can differ, so check a browser support table before you rely on it.
A page can first check that the browser has the basic Wasm tools at all. This is called feature detection.
const canUseWasm = typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function"; console.log(canUseWasm);console.log(typeof WebAssembly.Memory === "function");- Lines 1 and 2 check that
WebAssemblyexists and that it has aninstantiatefunction.typeofgives the kind of a value as text, such as"object"or"function". - Line 4 prints the answer:
true. - Line 5 checks that
WebAssembly.Memoryis a function. It printstruetoo.
This check only says the basic tools are there. It does not say that WasmGC or other newer features work. A real app should also test the exact module it plans to load.
WasmGC does not turn your JavaScript objects into Wasm objects. It gives compilers new object types for Wasm, and the browser cleans those up.
JSPI: Wasm waits for a Promise
Many browser tools, such as fetch, return a Promise: a value that will be ready later. But code written in C or Rust usually expects an answer right away. It does not know how to wait for a Promise.
JavaScript Promise Integration, or JSPI, solves this. Here is what happens:
- Wasm calls a JavaScript function that it imported.
- That function returns a Promise.
- The Wasm code pauses.
- When the Promise is ready, the Wasm code goes on.
// Only an example: a Wasm compiler usually sets up JSPI for you.async function askForPrice() { return Promise.resolve(12);} console.log(await askForPrice());- Line 1 is a comment: this is only an example. A Wasm compiler usually sets up JSPI for you.
- Line 2 starts an
asyncfunction. It returns a Promise that becomes12. - Line 5 waits with
awaitand prints12.
There is no Wasm in this code. It only shows the kind of JavaScript function JSPI works with. The compiler wraps the module's imports and exports when it loads. Then Wasm code that looks like it gets answers right away can pause and go on later.
The official Wasm feature list says JSPI is now part of the standard. But each browser adds it in its own version, so test it in the browsers you need.
JSPI only pauses Wasm code. It does not pause normal JavaScript. JavaScript already has async, await, and Promises for that.
Choosing Wasm for a real app
Start with a problem the user can feel, and measure it. Say a photo editor is slow when it resizes images:
- Profile the resize loop.
- If the loop takes most of the time, and it only works on numbers, it may be a good fit for Wasm.
- If a click is slow because of the network or because the page is redrawing, Wasm will not help.
The place where JavaScript and Wasm hand data to each other is called the boundary. Keep it small at first:
- Pass one typed array, or a few numbers.
- Call one export.
- Measure before and after.
Every crossing costs a little time. Turning big JavaScript objects into bytes, or copying large arrays, again and again can cost more than Wasm saves.
function resizeNearest(pixels, sourceWidth, targetWidth) { const result = new Uint8Array(targetWidth); for (let index = 0; index < targetWidth; index += 1) { const sourceIndex = Math.floor(index * sourceWidth / targetWidth); result[index] = pixels[sourceIndex]; } return result;} const pixels = new Uint8Array([10, 20, 30, 40]);console.log([...resizeNearest(pixels, 4, 2)].join(","));This shrinks a row of 4 pixels to 2 pixels. Each pixel is one byte.
- Line 1 is the kind of small function a Wasm module could export. It takes the pixels, the old width, and the new width.
- Line 2 makes a new typed array for the result.
- Lines 4 to 6 loop over the new pixels. For each one, they pick the nearest old pixel and copy it.
- Line 9 returns the result.
- Line 13 prints
10,30: pixels 0 and 2 of the old row.
This code is JavaScript so that it can run here. The point is the shape of the boundary:
- Pass the pixel bytes in once.
- Do all the repeated math together.
- Pass the result bytes back once.
The buttons and the preview stay in JavaScript. Only move the loop to Wasm after profiling shows it is what makes the user wait.
A few more Wasm words you may meet:
- WASI gives Wasm programs outside the browser a way to use things like files. It is not a file system inside the browser.
- SIMD lets one step work on several numbers at once.
- Exception handling lets compiled code throw and catch errors.
- The component model aims to let Wasm parts talk through richer types than numbers. It is still a proposal, and browsers do not have it.
Support for each of these differs, so check the places you plan to run your code.
The table below lists choices for a first boundary. Each row says what to do, an example, and why.
| What to decide | Example | Why |
|---|---|---|
| What to pass first | One Uint8Array of pixels and two width numbers. | One resize call gives back one array. |
| What to watch for | Turning JavaScript objects into bytes on every call. | It can use up all the time Wasm saves. |
| What to keep in JavaScript | Buttons, the preview, and the browser's tools. | They are made to be used from JavaScript. |
fetch("image.wasm")- The steps inside the exported
addfunction WebAssembly.instantiate(bytes)new Uint8Array(memory.buffer)- Changing the text on a button
- An
i32.addstep
Put each card on the JavaScript side, the Wasm side, or the boundary between them.
To run heavy work on several threads, see shared memory and parallel patterns. Those lessons explain how workers take turns. This lesson is about how JavaScript and Wasm pass data to each other.
Common misconceptions
- “Wasm replaces JavaScript.” No. Usually JavaScript calls Wasm for one focused job.
- “Wasm is always faster.” Not always. Passing data across the boundary takes time, the file must download, and the slow part may not be number work at all.
- “Wasm memory holds objects.” No. Linear memory is only bytes. Both sides must agree on which byte means what.
- “Every browser uses Liftoff.” No. Liftoff is the name of V8's compiler. Other engines have their own.
- “JSPI replaces async JavaScript.” No. JSPI only lets Wasm wait for a Promise that JavaScript returns.
The table below lists words that are easy to mix up. Each row says what the word means and what it does not mean.
| Word | What it is | What it is not |
|---|---|---|
| Wasm | A binary program format that a compiler makes and JavaScript runs. | A language you write by hand, or a JavaScript replacement. |
| Linear memory | One row of bytes that Wasm and JavaScript both read and write. | JavaScript objects shared field by field. |
| Liftoff | V8's quick first compiler for Wasm. | A compiler that every browser uses. |
| JSPI | A way for Wasm to pause until a JavaScript Promise is ready. | A way to pause normal JavaScript code. |
Practice exercises
Read the small program and type what it prints.
const add = (left, right) => left + right;
console.log(add(2, 3));It prints 5, because 2 + 3 is 5. The Wasm add export from the lesson gives the same answer.
Someone sent you some bytes. Before you load them, you want to check that they are a well-formed Wasm module. Which function do you call?
WebAssembly.validate(bytes)WebAssembly.validate checks that the bytes are a well-formed module. It does not make an instance or run anything.
This program makes a memory with one page. What does it print?
const memory = new WebAssembly.Memory({ initial: 1 });
console.log(memory.buffer.byteLength);It prints 65536. One Wasm memory page is 65536 bytes, which is 64 KiB.
A teammate grows Wasm memory with grow(1). Then they keep using the view they made before. Which byteLength number shows that the old view is detached?
The old view's byteLength is 0. Fix it by making a new Uint8Array from memory.buffer after grow.
A photo editor freezes for a moment while it resizes large images. Which part would you try moving to Wasm first?
Try the image resize loop first. It repeats the same number work, and it can take and return one typed array. Buttons and layout stay in JavaScript.
Wasm calls a JavaScript function that works in the background. JSPI pauses the Wasm code. What is it waiting for?
A Promise. JSPI pauses the Wasm code until the Promise from JavaScript is ready, then goes on.
Check your understanding
For each question, ask: is this a JavaScript job, a Wasm job, or the boundary between them? Who loads the module, who does the number work, and which bytes do they share?
Question 1 of 8What is WebAssembly mainly for?
Choose an answer to see the explanation.
Question 2 of 8What does this print?
Read the code, then predictconst add = (left, right) => left + right; console.log(add(2, 3));Choose an answer to see the explanation.
Question 3 of 8Which tool only checks that bytes are a well-formed module?
Choose an answer to see the explanation.
Question 4 of 8Wasm memory grows with
grow(1). What happens to aUint8Arrayview made before that?Choose an answer to see the explanation.
Question 5 of 8Why does V8 use Liftoff before TurboFan for Wasm?
Choose an answer to see the explanation.
Question 6 of 8What does WasmGC give to languages such as Java, Kotlin, and Dart?
Choose an answer to see the explanation.
Question 7 of 8What problem does JSPI (JavaScript Promise Integration) solve?
Choose an answer to see the explanation.
Question 8 of 8An image editor wants to try Wasm. What is the best first step?
Choose an answer to see the explanation.
Key takeaways
- Wasm is a binary program format. A compiler makes it, and JavaScript loads it and calls it for one focused job.
validatechecks the bytes.instantiateloads bytes you already have.instantiateStreamingloads a file while it downloads.- V8 uses Liftoff to make machine code quickly, and TurboFan to make faster code for busy functions.
- Linear memory is one row of bytes that both sides share. After
grow, make a new view. - WasmGC lets languages such as Java, Kotlin, and Dart use the browser's garbage collector for their Wasm objects.
- JSPI lets Wasm code pause while it waits for a JavaScript Promise.
Remember the one-liner.
JavaScript runs the app. Wasm is a fast helper for number work that JavaScript loads, calls, and shares bytes with.
Coming next: How to read ECMAScript