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.
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).
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-optwithout 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.
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.
Related
- Reading the glue code wasm-bindgen generates — what changes in the glue.
- Reference types and externref — the proposal.
- Migrating from old wasm-bindgen versions — upgrading to get it.
- Passing JS objects to Rust with wasm-bindgen — the API it speeds up.
← Back to wasm-bindgen Deep Dive