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.

Deciding from the destination, not the technology A browser-only Rust project is best served by wasm-bindgen today. A project where several languages must implement or consume the same interface, or where WASI preview 2 is the target, is better served by the component model. Mixed cases can use both. where does this run? a browser, Rust only web-sys, DOM access, mature tooling, no transpile wasm-bindgen a runtime, or several languages one interface, many implementations WASI preview 2 targets component model both a component for the server, bindgen glue for the browser one core crate, two wrappers The rightmost option is more common than it looks: keep the computation in a plain library crate and put the binding layer at the edges.

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.

Where each approach is ahead wasm-bindgen leads on browser ergonomics and tooling maturity. The component model leads on language neutrality and composition. Glue size and boundary cost are comparable between them. wasm-bindgen component model browser ergonomics tooling maturity language neutrality composition glue size / boundary cost Longer is better. The bottom row is a tie, which is worth knowing because performance is often assumed to be the deciding factor and rarely is.

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.

Structure so the choice stays reversible Keeping the computation in a binding-free crate means adding the second wrapper later is a small piece of work rather than a migration. The tests live with the core, where no binding layer is involved at all. crates/core algorithm + tests, no bindings crates/browser #[wasm_bindgen] wrappers crates/component WIT world + cargo-component Either wrapper can be added, replaced or deleted without touching the code that does the work — which is what makes the decision low-stakes.

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.

← Back to Wasm Component Model & WIT Bindings