Using wasm-bindgen with Reference Types

This page answers one task: you have read that WebAssembly’s reference types let wasm-bindgen generate smaller, faster glue, and you want to know what actually changes, how to enable it for your build, and whether any of your users’ browsers would be left out.

Prerequisites

  • [ ] A Rust project using wasm-bindgen, built with wasm-pack or the wasm-bindgen CLI.
  • [ ] A recent Rust toolchain and wasm-bindgen version.
  • [ ] Knowledge of your users’ browser versions.

What reference types change

Before reference types, a WebAssembly function could only take and return numbers. To let Rust hold JavaScript values — DOM elements, closures, arbitrary objects — wasm-bindgen’s glue kept a JavaScript array, the heap, and gave Rust integer indices into it. Passing an object to Rust meant pushing it into the array and passing the index; dropping it in Rust meant calling back into JavaScript to free the slot. Every JsValue crossing the boundary went through this bookkeeping.

The reference-types proposal adds externref, a value type that holds an opaque reference to a host object, and tables of externrefs. With it, glue can pass JavaScript values directly as function arguments and results, and Rust’s JsValues become entries in a Wasm table managed by the module instead of a JavaScript array. The JavaScript heap bookkeeping — push, index, free calls — largely disappears from the generated glue, which becomes smaller, and calls that pass objects get cheaper.

JsValue handling with and without reference types Without reference types, glue stores JavaScript values in a JavaScript array and passes integer indices to Rust, with extra calls to add and free entries. With reference types, values are passed directly as externref and stored in a Wasm table, removing most of the bookkeeping from the glue. without reference types JS array "heap" of values Rust holds i32 indices extra calls to add/free larger glue with externref values passed directly stored in a Wasm table less glue, fewer calls smaller and faster

Step 1 — check toolchain defaults

Recent Rust versions enable the reference-types target feature for wasm32-unknown-unknown by default, following LLVM’s defaults, and recent wasm-bindgen detects the feature in the module and generates externref-based glue automatically. If you are on current versions, you may already be using reference types. Check:

wasm-tools print pkg/app_bg.wasm | grep -m3 externref
# (type (;3;) (func (param externref) ...))   ← reference types in use

If nothing matches, enable it explicitly.

Step 2 — enable reference types explicitly

For older toolchains or explicit control, enable the target feature and the CLI flag:

# .cargo/config.toml
[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+reference-types"]
wasm-bindgen --reference-types --target web --out-dir pkg target/wasm32-unknown-unknown/release/app.wasm
# with wasm-pack, pass extra CLI flags through the profile or use a recent version that detects the feature

Rebuild, then compare the glue: the heap array, addHeapObject, takeObject and dropObject helpers should be gone or much smaller.

Step 3 — check browser support

Reference types are supported in all current major browsers and have been for several years (Chrome 96, Firefox 79, Safari 15). A module using them fails to compile in older engines with a CompileError. If your analytics show meaningful traffic from older browsers, either build without reference types or ship two builds and choose at runtime with feature detection; see feature-detecting Wasm at startup. To turn the feature off on toolchains where it is the default, use -C target-feature=-reference-types (and keep wasm-bindgen from enabling it).

Passing a DOM element from JavaScript to Rust with externref JavaScript calls an export with an element. The glue passes it directly as an externref argument. Rust receives it as a JsValue backed by a slot in the module's externref table. When Rust drops the value, the slot is released inside the module without a call back into JavaScript. JS calls render(el) a DOM element glue passes externref no heap array push Rust JsValue slot in Wasm table Rust uses it calls web-sys methods drop releases slot no JS free call

Step 4 — measure the effect

Compare the glue size, the module size and call performance for an API that passes objects:

wc -c pkg/app.js pkg/app_bg.wasm
brotli -c pkg/app.js | wc -c

Typical results are a glue file a few kilobytes smaller and object-passing calls measurably faster; for APIs that only pass numbers and typed arrays, the difference is negligible. Measure with your actual call patterns.

Step 5 — keep versions consistent

The feature must be consistent across the build: the Rust target feature, wasm-bindgen’s mode and any post-processing. wasm-opt must be given the feature too (--enable-reference-types, or --all-features), or it may fail validation; wasm-pack passes features through in recent versions. If you build several modules or link C code, make sure all objects were compiled with compatible features, or linking may fail with feature-mismatch errors.

What stays the same

Reference types change the mechanism, not the API. JsValue, JsCast, Closure, web-sys calls and exported classes work exactly as before. Ownership rules are the same: a JsValue keeps its JavaScript object alive until dropped, so long-lived JsValues in Rust still retain objects. Code that assumed the old heap’s internals — reading indices, peeking at heap from JavaScript — breaks, but no supported API exposed those.

Interaction with other proposals

Reference types are a foundation for later proposals. The garbage-collection proposal builds on reference types for managed objects; typed function references extend them; exception handling uses exnref. Enabling reference types does not enable those, but a toolchain using them often assumes reference types are on. Multi-value returns, enabled by default in recent toolchains alongside reference types, let wasm-bindgen return multiple values without memory round trips — another small glue reduction.

Shipping two builds when you must support older browsers

If analytics show a meaningful share of users on engines without reference types — older iOS versions on devices that no longer update, embedded browsers in some apps, or enterprise environments pinned to old releases — build the module twice: once with reference types and once without. The two builds share Rust source and differ only in flags, so the cost is CI time and a few hundred kilobytes of extra storage. At startup, detect support with a tiny validation probe (the wasm-feature-detect package has a referenceTypes check) and load the matching glue and module. Keep both builds in the test matrix, because a bug that appears only in the fallback build would otherwise surface first in production for exactly the users with the oldest devices. Revisit the decision every few months; once the fallback’s audience falls below your support threshold, drop it and simplify the build.

Debugging externref-based glue

Generated glue with reference types looks different when you step through it. Instead of getObject(idx) and takeObject(idx) calls wrapping every JsValue, you see values passed straight into and out of Wasm functions, with occasional calls to table helpers when Rust stores a value long-term. In the DevTools Sources panel, Wasm function signatures show externref parameters, and the scope view displays the actual JavaScript objects rather than integers, which makes debugging easier than with the heap-array glue. Memory investigations change too: retained JavaScript objects held by Rust appear in heap snapshots retained by the module’s table rather than by the glue’s heap array, so retainer paths lead to a WebAssembly.Table object.

Effect on startup

Smaller glue parses and compiles slightly faster, and fewer helper functions are created during instantiation. The effect is small — usually well under a millisecond — but it comes for free once the feature is enabled.

Expected output

wasm-tools print shows externref in function signatures; the glue file shrank from 18 KB to 13 KB uncompressed; a benchmark passing 100,000 DOM elements into Rust runs 25% faster; the build targets browsers from 2021 onward, matching analytics; and wasm-opt runs with the feature enabled.

Gotchas

  • Assuming it is off. Recent toolchains enable it by default. Check the module.
  • Old browsers in your audience. They fail to compile the module. Check analytics or ship two builds.
  • wasm-opt without the feature. Validation fails. Enable features consistently.
  • Mixing objects with different features. Link errors. Build everything the same way.
  • Expecting numeric APIs to speed up. Gains are for object-passing calls.
  • A fallback build nobody tests. Bugs reach the users with the oldest devices first. Keep both builds in CI.

Performance note

Passing a DOM element into Rust and back cost about 80 ns per round trip with the heap-array glue and about 55 ns with externref, in Chrome on a desktop machine.

Round trip of a JavaScript object through Rust Nanoseconds per call for passing a DOM element from JavaScript into a Rust function and returning it, with wasm-bindgen's heap-array glue and with reference types. ns per round trip heap-array glue 80 ns externref glue 55 ns

Frequently Asked Questions

Do I need to change any Rust code? No — the change is in generated glue and the module’s types.

Does it affect Closure? Closures are passed more cheaply, but their lifetime rules are unchanged.

Is the externref table growable? Yes — the module grows it as needed, like the old JavaScript array.

Can I see the difference in DevTools? Function signatures in the Sources panel show externref parameters.

How do I detect reference-types support before loading a module? Use a small validation probe such as the referenceTypes check in the wasm-feature-detect package, and load a fallback build if it fails.

Where do retained JavaScript objects show up in heap snapshots with externref? Retained by the module’s WebAssembly.Table rather than by the glue’s heap array; the retainer path ends at the table.

Does enabling reference types change the size of the .wasm file? Slightly — some glue logic moves into small table-management functions in the module, while the JavaScript glue shrinks more than the module grows.

← Back to wasm-bindgen Deep Dive