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 moduleValidate WebAssembly bytes, instantiate a small module, and call an exported function from JavaScript.
- 02Explain the boundaryDescribe imports, exports, linear memory, and why old views detach when non-shared memory grows.
- 03Choose the right toolRecognize Wasm's best fit, V8 tiering, WasmGC, and JS Promise Integration without treating Wasm as a JavaScript replacement.
What WebAssembly is
WebAssembly, often called Wasm, is a compact binary instruction format that browsers and other runtimes can execute. It is normally produced by a compiler from another language, then loaded and called by JavaScript. It is a teammate for JavaScript, not a new way to write every web page.
Use it when a small part of an app spends a lot of time doing predictable numeric work: resizing an image, decoding video, processing audio, running a game loop, or calculating a large model. Keep page layout, buttons, forms, network requests, and most application logic in JavaScript where browser APIs are designed to be used.
A browser treats a Wasm module as a small, typed virtual machine program inside its JavaScript engine. The module can only use the imports and memory it was given, and the engine validates its structure before it runs. That makes it portable across machines while still letting the engine compile its instructions into fast local machine code.
Do not reach for Wasm because a task merely sounds technical. First profile the real page. A short DOM update, a network wait, or a database request is often the bottleneck; a compact repeated calculation is the more promising candidate.
Wasm is a typed, portable binary format. JavaScript loads a module, gives it imports when needed, calls its exports, and can share a byte buffer called linear memory.
| Question | JavaScript | WebAssembly |
|---|---|---|
| Best at | Browser APIs and app coordination | Focused numeric computation |
| Usually written as | Source code you maintain directly | Binary output from a compiler |
| Data at the boundary | Objects, typed arrays, and numbers | Numbers and linear-memory bytes |
| First question | Which user action needs work? | Is this measured work compact and repeated? |
A flexible home cook can prepare a whole meal. A food processor is very fast at the job it was built for, but the cook still plugs it in, puts food in, and serves the result.
- In real life: The cook chooses what to make
- In JavaScript: JavaScript coordinates the app
- In real life: The food processor handles one repeated job
- In JavaScript: Wasm handles focused numeric work
- In real life: The cook plugs it in and adds food
- In JavaScript: JavaScript loads and calls the module
- In real life: The cook serves the result
- In JavaScript: JavaScript uses the returned value
Where the analogy stops: A food processor is not always faster: moving data across the boundary and loading code have costs, so measure a real hot path first.
The next lesson is not published yet. This is the last lesson in this stage, so the trail pauses here rather than guessing at a link. The earlier typed arrays lesson is useful preparation because typed arrays also expose bytes with clear numeric types.
A tiny Wasm module
The smallest useful way to meet Wasm is a function that adds two integers. The bytes below are a complete module. They start with the magic header \0asm, then contain a version, a function type, one function, an export named add, and the instructions that read two values 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));Line 1 creates a Uint8Array holding the module bytes. Line 2 asks the WebAssembly API to validate, compile, and instantiate those bytes. Line 3 calls the exported add function. The real output is 5.
The first eight bytes are the \0asm header and version. The next sections say what function shape exists, which function uses that shape, which public name exposes it, and which instructions implement it. A section has an ID byte, a byte length, then its payload, so the engine can skip 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, 18], ["function", 18, 22], ["export", 22, 31], ["code", 31, 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 through 7 label byte ranges: header and version, type, function, export, and code. The loop on line 10 prints each label with its size, so it shows header and version 8 first and code 10 last. The labels are a teaching map, not a general binary parser.
You usually do not write these bytes by hand. A compiler emits them. The text format below, called WAT, is a readable version of the same simple idea. It declares two 32-bit integer parameters, adds them, and exports the function.
(module
(func $add (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
(export "add" (func $add)))This is a replay of the lesson's real add-module call, not an engine debugger.
script
const { instance } = await WebAssembly.instantiate(bytes);console.log(instance.exports.add(2, 3));This is a guided replay of the real module layout, not a Wasm binary parser.
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, 18], ["function", 18, 22], ["export", 22, 31], ["code", 31, 41],]; for (const [name, start, end] of sections) { console.log(name, bytes.slice(start, end).length);}Loading and calling modules
Instantiation creates a usable instance of a module. An instance has exports: functions, memories, tables, or globals that JavaScript can use. Before you instantiate unknown bytes, WebAssembly.validate can cheaply tell you whether they have valid Wasm structure.
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 contains the valid add module. Line 2 contains only a partial header. Lines 3 and 4 print true and false. Validation does not run the module and does not promise that its behavior is useful; it only checks the Wasm format.
| API | What it does | When it fits |
|---|---|---|
WebAssembly.validate(bytes) | Checks whether bytes form a valid module. | Returns a boolean; it does not create an instance. |
WebAssembly.instantiate(bytes) | Compiles and instantiates bytes already in memory. | Useful for an ArrayBuffer or Uint8Array you already have. |
WebAssembly.instantiateStreaming(fetch(url)) | Streams a fetched response into compilation and instantiation. | Use it when the server serves a .wasm file with the right MIME type. |
| An instance export | A function or memory provided by Wasm to JavaScript. | Call an exported function like instance.exports.add(2, 3). |
When bytes come from a file, streaming lets the browser compile while the response arrives. The server must deliver the file with the correct application/wasm MIME type, so this example is a fragment rather than an editor-run snippet.
const { instance } = await WebAssembly.instantiateStreaming(
fetch("add.wasm"),
);
console.log(instance.exports.add(2, 3));Line 1 starts a fetch and hands its response to instantiateStreaming. Line 4 calls the export after the awaited instance exists. Imports flow in the other direction: JavaScript can supply functions or memory in the second argument to instantiation, and Wasm can call those imports through its declared interface.
Liftoff and tiered compilation
Wasm bytes are not machine code for every computer. In V8, the engine turns Wasm instructions into machine code. Liftoff is V8's fast baseline compiler: it makes usable code quickly, so an application can start. It is deliberately simple and has less room for deep optimization.
function chooseTier(calls) { return calls >= 3 ? "TurboFan" : "Liftoff";} console.log(chooseTier(1));console.log(chooseTier(3));Line 1 models a call count. Line 2 chooses the baseline name before three calls and the optimizing name from three calls onward. The model prints Liftoff and then TurboFan. It is only a teaching model: real engines use their own thresholds and scheduling.
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 1 describes code that has not run. Line 2 labels early calls as a Liftoff baseline, and line 3 says later calls are a TurboFan candidate. The three logs show the timeline without pretending that V8 upgrades at a fixed call count.
V8's current pipeline documentation says functions are initially compiled by Liftoff when called, then hot functions can be recompiled by TurboFan on a background thread. TurboFan takes more passes and can improve register allocation, eliminate redundant work, and inline. A currently running Liftoff call finishes as Liftoff code; later calls can use optimized code. See tiered compilation for the same idea across JavaScript engines.
Liftoff favors startup speed. TurboFan favors sustained speed for work that stays hot. The important user-facing result is start quickly, then improve the expensive path without asking the app author to switch APIs.
Sharing linear memory
Linear memory is a contiguous buffer of bytes. A Wasm module can import a memory created by JavaScript, or export a memory it created. JavaScript uses a typed-array view such as Uint8Array to read and write those exact bytes. This avoids copying every byte through a separate JavaScript object.
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 creates one page. A Wasm page is 65536 bytes, so line 4 first prints 65536 42. Line 5 grows by one page. Line 6 then prints 131072 0: the current memory is two pages and the old non-shared view is detached.
After grow, make a fresh view from memory.buffer. For multi-byte numbers, use DataView and specify little-endian order because Wasm memory is always little-endian. A shared Wasm memory is a separate advanced feature with stronger browser security and synchronization requirements.
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 creates the bytes and line 2 gives JavaScript a DataView. Line 4 writes the number 500 in little-endian order. Lines 5 and 6 print 244 1 and 500: the low byte comes first, while the view reconstructs the original number.
JavaScript and Wasm share one cutting board. They read and write the same bytes instead of passing copies back and forth.
- In real life: Two cooks use one board
- In JavaScript: JavaScript and Wasm use one memory buffer
- In real life: A cook places a chopped piece at a position
- In JavaScript: A typed view writes a byte index
- In real life: Both cooks see the same piece
- In JavaScript: Both sides read the same bytes
- In real life: A bigger board replaces the old board
- In JavaScript: Memory growth detaches old ordinary views
Where the analogy stops: Bytes have no built-in object shape. JavaScript and Wasm must agree on indexes, numeric types, and layout.
This replay runs the lesson's real memory-growth function. It shows JavaScript-visible results, not internal engine storage.
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 writes a byte through a Uint8Array view.
131072One more Wasm page adds 65536 bytes.
0Use a fresh view after ordinary memory growth.
Index 12 stores 42. One page is 65536 bytes; after growth it is 131072 bytes and the old view has 0 bytes.
WasmGC
Traditional Wasm modules use linear memory, which is untyped bytes. Languages with their own garbage collectors often compile that collector into the module too. WasmGC adds VM-managed structs and arrays so a toolchain can represent objects using the host's garbage collector instead.
This is especially useful for garbage-collected languages such as Kotlin, Dart, and Java. Rather than shipping a second collector and object runtime in every Wasm file, a compatible toolchain can use the browser VM's managed objects. That can reduce code size and lets the VM understand object types more directly.
It is still a compiler-target decision, not a JavaScript feature you sprinkle into app code. A language needs a toolchain that emits WasmGC instructions, and its runtime semantics still need careful design. V8 reported WasmGC shipped in Chrome 119; do not assume one engine's version tells you every browser's support without checking its compatibility data.
A web app can feature-detect the basic Wasm API before choosing an optional path. That check answers only whether the host exposes the core JavaScript interface; a production app should still test the exact module or advanced proposal it plans to load.
const canUseWasm = typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function"; console.log(canUseWasm);console.log(typeof WebAssembly.Memory === "function");Lines 1 and 2 check the global object and its instantiate method. Line 4 prints whether the basic API is available, and line 5 separately checks the memory constructor. Both print true in a modern browser with WebAssembly support.
WasmGC does not make ordinary JavaScript objects automatically become Wasm objects. It gives a Wasm compiler higher-level GC types so the host VM can manage those Wasm-side objects.
JavaScript Promise Integration
Web APIs such as fetch return Promises, but many native programs are written in a straight, synchronous style. JavaScript Promise Integration, or JSPI, lets a Wasm call suspend when an imported JavaScript function returns a Promise, then resume when that Promise settles.
// Illustration only: JSPI is usually produced by a Wasm toolchain.async function askForPrice() { return Promise.resolve(12);} console.log(await askForPrice());Line 2 returns a Promise that resolves to 12. Line 5 awaits it and prints 12. This is not a hand-written JSPI module; it is the small Promise shape that a Wasm toolchain can bridge. Toolchains wrap imports and exports during instantiation so a synchronous-looking Wasm function can pause and later continue.
The V8 JSPI announcement says the feature reached WebAssembly phase 4 and is available in Chrome 137 and Firefox 139. JSPI does not suspend normal JavaScript call stacks. JavaScript already has async, await, and Promises for its own asynchronous code.
Choosing Wasm for a real app
Start with a user-visible problem and a measurement. If image resizing makes a photo editor slow, profile the resize loop. If it dominates time and has a compact numeric interface, it may be a useful Wasm candidate. If a click handler is slow because of network latency or DOM layout, Wasm is unlikely to help.
Keep the first boundary small. Pass one typed array or a few numbers, call one export, and measure before and after. Repeatedly converting complicated JavaScript objects or copying large arrays can erase the gain from faster compute work.
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(","));Line 1 models the compact resize function a module could export. Line 2 creates the output pixels, lines 4 through 6 choose each source pixel, and line 13 logs 10,30. A real app can keep its buttons and preview in JavaScript while passing one image buffer to a Wasm resize routine.
The model is deliberately JavaScript so it can run here. The design lesson is the boundary: pass typed data once, perform the repeated math together, then return typed data once. Replace it with compiled Wasm only after profiling shows that this particular loop dominates the user-visible delay.
| Decision | Example | Why |
|---|---|---|
| Good first boundary | A pixel Uint8Array and width numbers | One resize call returns one output array |
| Cost to watch | Repeated object conversion | It can erase the compute gain |
| Keep in JavaScript | Buttons, previews, and browser APIs | They are already JavaScript-facing |
fetch("image.wasm")- The exported
addfunction body WebAssembly.instantiate(bytes)new Uint8Array(memory.buffer)- Updating a button label
- An
i32.addinstruction
Sort each card by the JavaScript side, the Wasm side, or the shared boundary.
For concurrency around heavy work, see shared memory and parallel patterns. Those lessons explain worker coordination; this one explains the JavaScript-to-Wasm boundary.
Common misconceptions
- “Wasm replaces JavaScript.” It is usually a focused compute component called from JavaScript.
- “Wasm is automatically faster.” Boundary overhead, download size, and the actual bottleneck matter.
- “A Wasm page is an object heap.” Linear memory is bytes; code must agree on layout.
- “All engines use Liftoff.” Liftoff is V8 terminology, so describe it as a V8 pipeline detail.
- “JSPI replaces async JavaScript.” It bridges Wasm to Promise-returning JavaScript APIs.
| Term | What it is | Do not confuse it with |
|---|---|---|
| Wasm | A compact, typed binary instruction format and runtime target. | A replacement for JavaScript or a source language. |
| Linear memory | A byte buffer that Wasm and JavaScript can both access. | JavaScript objects automatically shared field by field. |
| Liftoff | V8's fast baseline Wasm compiler. | A guarantee every browser uses the same compiler pipeline. |
| JSPI | A boundary API that suspends and resumes Wasm around Promises. | A way to suspend ordinary JavaScript frames. |
Practice exercises
Read the tiny program and type its output.
const add = (left, right) => left + right;
console.log(add(2, 3));It prints 5. The add export in the Wasm example produces the same numeric result.
You received bytes before loading a module. Which API checks whether they have valid Wasm structure?
WebAssembly.validate(bytes)validate checks the module's binary structure without creating an instance.
What does this program print for one initial Wasm memory page?
const memory = new WebAssembly.Memory({ initial: 1 });
console.log(memory.buffer.byteLength);It prints 65536. A Wasm memory page is 64 KiB.
A teammate grows ordinary Wasm memory and keeps using their old typed-array view. What value reveals that the old view is detached?
The old view has byteLength 0. Make a new Uint8Array from memory.buffer after growth.
A photo editor stutters while resizing large images. Which part would you consider moving to Wasm first?
Try the image resize loop first. It is repeated numeric work that can accept and return typed-array data, unlike button and layout code.
What does JSPI suspend and resume around when a Wasm program calls an asynchronous JavaScript import?
JSPI suspends and resumes Wasm around a Promise returned by JavaScript.
Check your understanding
Answer by tracing the JavaScript-to-Wasm boundary: who loads, who computes, which bytes are shared, and when a feature belongs in a toolchain.
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 API only validates module bytes?
Choose an answer to see the explanation.
Question 4 of 8What happens to an old Uint8Array view after ordinary Wasm memory grows?
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 enable for supported toolchains?
Choose an answer to see the explanation.
Question 7 of 8What problem does JS Promise Integration address?
Choose an answer to see the explanation.
Question 8 of 8Which boundary is the best first Wasm experiment for an image editor?
Choose an answer to see the explanation.
Key takeaways
- Wasm is a typed binary target that JavaScript loads and calls for focused work.
- Use validation for format checks, instantiation for in-memory bytes, and streaming for fetched modules.
- V8 uses Liftoff for quick baseline Wasm code and TurboFan for hot-code optimization.
- Linear memory is shared bytes; make a new view after ordinary growth.
- WasmGC helps compatible garbage-collected language toolchains use VM-managed Wasm objects.
- JSPI bridges synchronous-looking Wasm code to Promise-returning JavaScript APIs.
Remember the one-liner.
JavaScript runs the app; WebAssembly is a focused compute tool JavaScript can load, call, and share bytes with.
Coming next: How to read ECMAScript