cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

JavaScript & WebAssembly

Learn when WebAssembly helps, how JavaScript loads and calls it, how memory is shared, and how modern Wasm features fit together.

By the end, you can
  • 01
    Load a moduleValidate WebAssembly bytes, instantiate a small module, and call an exported function from JavaScript.
  • 02
    Explain the boundaryDescribe imports, exports, linear memory, and why old views detach when non-shared memory grows.
  • 03
    Choose 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.

Definition

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.

JavaScript and Wasm have different jobs
QuestionJavaScriptWebAssembly
Best atBrowser APIs and app coordinationFocused numeric computation
Usually written asSource code you maintain directlyBinary output from a compiler
Data at the boundaryObjects, typed arrays, and numbersNumbers and linear-memory bytes
First questionWhich user action needs work?Is this measured work compact and repeated?
Real-life analogyA home cook and a food processor

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.

Instantiate a Wasm add modulePop out in the code editor (opens in a new tab)JavaScript
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.

Name the five parts of the add modulePop out in the code editor (opens in a new tab)JavaScript
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.

The same idea in WebAssembly text formatWebAssembly text
(module
  (func $add (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)
  (export "add" (func $add)))
Step through calling the Wasm add export
Step 0 of 4Ready
Your turn: follow the blue line

This is a replay of the lesson's real add-module call, not an engine debugger.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const { instance } = await WebAssembly.instantiate(bytes);console.log(instance.exports.add(2, 3));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Step through the add module's five sections
Step 0 of 5Ready
Your turn: follow the blue line

This is a guided replay of the real module layout, not a Wasm binary parser.

Running in
  1. script
Next: line 3
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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);}
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Validate good and broken Wasm bytesPop out in the code editor (opens in a new tab)JavaScript
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.

Loading choices
APIWhat it doesWhen 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 exportA 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.

Stream a fetched Wasm fileJavaScript
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.

A tiny teaching model of tier choicePop out in the code editor (opens in a new tab)JavaScript
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.

Describe the two-tier journeyPop out in the code editor (opens in a new tab)JavaScript
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.

Two tiers, one goal

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.

Create, write, and grow Wasm memoryPop out in the code editor (opens in a new tab)JavaScript
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.

Read a two-byte value in Wasm byte orderPop out in the code editor (opens in a new tab)JavaScript
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.

Real-life analogyOne cutting board

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.

Step through growing a memory page
Step 0 of 5Ready
Your turn: follow the blue line

This replay runs the lesson's real memory-growth function. It shows JavaScript-visible results, not internal engine storage.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Playground: write one shared memory byte
Memory codeJavaScript
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);
Memory result65536 bytes first
value at index 1242

JavaScript writes a byte through a Uint8Array view.

after grow131072

One more Wasm page adds 65536 bytes.

old view byteLength0

Use a fresh view after ordinary memory growth.

Try it yourself

Index 12 stores 42. One page is 65536 bytes; after growth it is 131072 bytes and the old view has 0 bytes.

This runs the lesson's real memory function. The output is a WebAssembly.Memory result, not a picture of browser internals.

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.

Detect the basic WebAssembly JavaScript APIPop out in the code editor (opens in a new tab)JavaScript
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.

What WasmGC does not mean

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.

A JavaScript Promise shape used by JSPIPop out in the code editor (opens in a new tab)JavaScript
// 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.

Keep an image-resize boundary to one typed arrayPop out in the code editor (opens in a new tab)JavaScript
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.

Keep the first boundary small
DecisionExampleWhy
Good first boundaryA pixel Uint8Array and width numbersOne resize call returns one output array
Cost to watchRepeated object conversionIt can erase the compute gain
Keep in JavaScriptButtons, previews, and browser APIsThey are already JavaScript-facing
Where does this part belong?
  • fetch("image.wasm")
  • The exported add function body
  • WebAssembly.instantiate(bytes)
  • new Uint8Array(memory.buffer)
  • Updating a button label
  • An i32.add instruction
Try it yourself
0 of 6 correct

Sort each card by the JavaScript side, the Wasm side, or the shared boundary.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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.
Terms that are easy to mix up
TermWhat it isDo not confuse it with
WasmA compact, typed binary instruction format and runtime target.A replacement for JavaScript or a source language.
Linear memoryA byte buffer that Wasm and JavaScript can both access.JavaScript objects automatically shared field by field.
LiftoffV8's fast baseline Wasm compiler.A guarantee every browser uses the same compiler pipeline.
JSPIA boundary API that suspends and resumes Wasm around Promises.A way to suspend ordinary JavaScript frames.

Practice exercises

Exercise 1 · Warm-upPredict the add output

Read the tiny program and type its output.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const add = (left, right) => left + right;
console.log(add(2, 3));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upChoose the format check

    You received bytes before loading a module. Which API checks whether they have valid Wasm structure?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeName one page size

      What does this program print for one initial Wasm memory page?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const memory = new WebAssembly.Memory({ initial: 1 });
      console.log(memory.buffer.byteLength);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeRepair a stale view

        A teammate grows ordinary Wasm memory and keeps using their old typed-array view. What value reveals that the old view is detached?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeApply Wasm to a photo editor

          A photo editor stutters while resizing large images. Which part would you consider moving to Wasm first?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeName the JSPI pause point

            What does JSPI suspend and resume around when a Wasm program calls an asynchronous JavaScript import?

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              JavaScript & WebAssembly quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What is WebAssembly mainly for?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const add = (left, right) => left + right;
                console.log(add(2, 3));

                Choose an answer to see the explanation.

              3. Question 3 of 8Which API only validates module bytes?

                Choose an answer to see the explanation.

              4. Question 4 of 8What happens to an old Uint8Array view after ordinary Wasm memory grows?

                Choose an answer to see the explanation.

              5. Question 5 of 8Why does V8 use Liftoff before TurboFan for Wasm?

                Choose an answer to see the explanation.

              6. Question 6 of 8What does WasmGC enable for supported toolchains?

                Choose an answer to see the explanation.

              7. Question 7 of 8What problem does JS Promise Integration address?

                Choose an answer to see the explanation.

              8. 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

              CompleteFrontend Clear concepts. Working examples.