Component Model vs wasm-bindgen
This guide answers a decision that comes up as soon as a Rust project needs a JavaScript interface:
generate bindings with wasm-bindgen, or describe the interface in WIT and build a component. Both
solve the same problem — turning typed values into the numeric boundary and back — and they are
strongest in different situations.
The short answer for most browser projects today is wasm-bindgen, and the short answer for polyglot
or server-side work is the component model. The rest of this page is the reasoning, so the decision
survives contact with a specific project rather than resting on a slogan.
Prerequisites
- [ ] A clear statement of where the module will run and who will call it
- [ ] Knowledge of which languages are involved now, and which plausibly will be
- [ ] A sense of how much the interface will change — churn favours generated bindings either way
- [ ] Both toolchains installed, if you intend to prototype rather than reason
The comparison, dimension by dimension
Browser ergonomics
wasm-bindgen wins clearly today. It emits an ES module directly, web-sys covers the browser API
surface, closures and DOM event handlers work, and there is no transpilation step. A component reaches
the browser through jco, which works well but adds a build stage and a generated-JavaScript layer.
Language neutrality
The component model wins, decisively. A WIT interface is implementable by any language with component
tooling and callable by any host, with the canonical ABI guaranteeing agreement. wasm-bindgen’s ABI is
its own: a module built with it is practically Rust-plus-JavaScript only.
Composition
Only the component model offers it. Two components can be linked into one artifact ahead of time, with
one satisfying the other’s imports and no host in the middle. There is no wasm-bindgen equivalent.
Tooling maturity
wasm-bindgen is years ahead in deployment: more documentation, more examples, more edge cases already
hit by someone else. Component tooling is good in Rust and thinner elsewhere, and the specification is
still gaining features.
Glue size and boundary cost
Comparable. Both generate JavaScript that lifts and lowers, and both grow with the richness of the interface rather than the size of the module. Neither has a decisive advantage; measure if it matters.
Structuring a project that may need both
The lowest-risk arrangement keeps the decision out of the computation entirely:
crates/
core/ # no bindings, no I/O — the algorithm and its tests
browser/ # #[wasm_bindgen] wrappers over core
component/ # WIT world + cargo-component wrappers over core
Both wrappers are thin, both depend on the same crate, and the tests live where there are no bindings to get in the way. Adding the second wrapper later is then a day of work rather than a migration, which turns an irreversible-feeling decision into a reversible one.
Gotchas
Choosing the component model for a browser-only project. You get a transpilation step, a less
mature toolchain and no web-sys equivalent, in exchange for portability you are not using.
Choosing wasm-bindgen for a plugin interface. If third parties are expected to implement your
interface in their own language, a Rust-specific ABI makes that impossible. This is exactly what the
component model exists for.
Assuming components are faster. They are not — the boundary cost is comparable, and lists copy in both designs. Pick on interface requirements, not on a performance intuition.
Underestimating the churn. If the interface is unstable, generated bindings on both sides are worth more than the specific technology. Both approaches generate; the hand-written ABI is what to avoid.
Migrating for the future rather than the present. The component model is where the ecosystem is heading, which is a good reason to structure code so a move is cheap, and a poor reason to adopt immature tooling before it is needed.
Performance note
Boundary cost is comparable and the compute is identical, so performance rarely decides this. Where a
real difference appears is in glue size for a rich interface: wasm-bindgen can be tuned by narrowing
what you export and which web-sys features you enable, while jco output is generated from the WIT
with fewer levers. Neither difference is likely to be decisive next to the ergonomics and portability
arguments.
If a project is genuinely performance-critical at the boundary, the answer is not to choose between
these but to bypass both for the hot path — a raw (ptr, len) export alongside the generated surface,
exactly as described in
passing complex types across the boundary.
Three concrete scenarios
Abstract comparisons are less useful than worked cases, so here are three that come up repeatedly.
An image filter in a React application, written in Rust, browser only. wasm-bindgen wins without
much argument. The module needs web-sys for canvas access, the team is Rust-only, the artifact goes
straight into an existing bundler pipeline, and no third party will ever implement the interface. Using
components here adds a transpilation step and a less mature toolchain in exchange for portability the
project does not need.
A plugin interface for a desktop application, implementable by users in any language. The component model, clearly. The whole point is that someone else writes the implementation, possibly in a language you have never used, and it has to interoperate with a host you control. A Rust-specific ABI makes that impossible; a WIT interface makes it routine, and composition lets you bundle first-party plugins into one artifact.
A data-transformation library used by both a web application and a server-side pipeline. Both, via
the layered structure. The computation lives in a plain crate with no bindings. The web application
consumes a wasm-bindgen build; the pipeline consumes a component targeting wasm32-wasip2. Two thin
wrappers, one implementation, and each consumer gets the ergonomics appropriate to its environment. The
duplication is a few dozen lines and is far cheaper than forcing either side onto the other’s tooling.
The pattern across all three is that the destination and the audience decide, not the technology’s
merits in the abstract. A question that resolves the choice quickly: will anyone other than us
implement or consume this interface, in a language other than ours? If yes, the component model earns
its cost. If no, wasm-bindgen is very hard to beat today.
What would change the answer
Two developments would shift the balance, and both are worth watching rather than waiting for. The first is native component support in browsers, which would remove the transpilation step and most of the ergonomic gap. The second is maturity in non-Rust guest tooling, which would make the polyglot argument concrete for teams who currently have only one realistic guest language anyway.
Until then, the structure recommended above is the hedge: keep the computation binding-free, treat the wrapper as replaceable, and revisit the decision when the artifact you want to ship changes rather than when the ecosystem news does.
Frequently Asked Questions
Can one crate produce both artifacts? Yes, with the layout above. The wrappers are separate crates so their dependencies do not mix, and the core crate stays free of both.
Will wasm-bindgen be replaced by components?
Possibly, eventually, for some use cases. It is actively maintained and deeply embedded in the Rust web
ecosystem, so a project starting today is not choosing a dead end either way.
What about other languages targeting the browser?
Then the component model plus jco is often the only realistic route, because language-specific
binding generators of wasm-bindgen’s quality mostly do not exist outside Rust.
Can I migrate a wasm-bindgen project to components later?
Yes, and the cost depends almost entirely on how the code is structured rather than on its size. A
project whose computation lives in a binding-free crate needs a new wrapper and a WIT file; one where
#[wasm_bindgen] attributes are scattered through the implementation needs those separated first,
which is the real work. Doing that separation early is worthwhile regardless of whether you ever
migrate, because it also makes the code testable without a browser.
Do the two interoperate?
Not directly — a wasm-bindgen module and a component are different artifacts with different ABIs, and
neither can call the other without a host in between. A project using both uses them for different
consumers rather than together.
Which has better error messages?
wasm-bindgen, today, by a clear margin — a consequence of maturity rather than design.
Related
- Wasm component model & WIT bindings — the model this compares against.
- wasm-bindgen deep dive — what the alternative actually generates.
- Understanding the canonical ABI — the boundary cost on the component side.
- Choosing between Emscripten, wasm-pack and TinyGo — the equivalent decision one level down.
← Back to Wasm Component Model & WIT Bindings