Enabling Only the web-sys Features You Use

This page answers one task: a Rust crate uses web-sys to call browser APIs, and you want to enable exactly the features it needs — no more, no fewer — so that builds stay fast, the binary stays small, and adding a new API is a quick, predictable step.

Prerequisites

  • [ ] A crate depending on web-sys and wasm-bindgen.
  • [ ] Familiarity with Cargo features.

Why web-sys is split into features

web-sys contains bindings for every Web API described by the WebIDL files of browsers: thousands of interfaces, from Document and Element to GPUDevice, AudioWorkletNode and XrSession. Compiling all of them would take minutes and generate an enormous amount of code. So every interface is behind its own Cargo feature, named after the interface: Window, Document, HtmlCanvasElement, CanvasRenderingContext2d. Nothing is enabled by default. A crate enables the interfaces it uses, and only those are compiled.

Two consequences follow. Forgetting a feature produces a compile error, because the type or method does not exist without it. And methods that mention another interface in their signature — Document::get_element_by_id returns an Element — are only generated when both interfaces’ features are enabled. That is the source of most confusion: a method you can see in the documentation seems to be missing because one of the types it mentions is not enabled.

How a web-sys method becomes available A method is generated only when the feature for its interface and the features for every interface in its signature are enabled. Document::get_element_by_id needs both the Document and Element features; with only Document enabled, the method does not exist. your code calls doc.get_element_by_id() Document feature interface enabled Element feature return type enabled method generated compiles

Step 1 — start with the minimal list

List only what the code uses, in Cargo.toml:

[dependencies.web-sys]
version = "0.3"
features = [
  "Window",
  "Document",
  "Element",
  "HtmlCanvasElement",
  "CanvasRenderingContext2d",
]
use wasm_bindgen::JsCast;

pub fn context() -> web_sys::CanvasRenderingContext2d {
    let doc = web_sys::window().unwrap().document().unwrap();
    let canvas: web_sys::HtmlCanvasElement = doc.get_element_by_id("c").unwrap().dyn_into().unwrap();
    canvas.get_context("2d").unwrap().unwrap().dyn_into().unwrap()
}

window() needs Window; document() needs Document; get_element_by_id needs Element for its return type; the casts need the target types.

Step 2 — read the documentation’s feature notes

Every item in the web-sys documentation on docs.rs ends with a note such as “This API requires the following crate features to be activated: Document, Element”. When adding a call, copy those names into the feature list. Searching the docs for the method name is faster than guessing from the error.

Event listeners are a common example. Adding a click handler needs EventTarget for add_event_listener_with_callback, Event or MouseEvent for the callback’s argument, and often Element or HtmlElement for the target itself — three or four features for one line of code. Writing the call first and then reading the docs for every type in the chain is quicker than enabling features one compile error at a time.

Step 3 — let the compiler tell you what is missing

When a feature is missing, the error says what does not exist, not which feature to add:

error[E0599]: no method named `get_element_by_id` found for struct `Document` in the current scope

The fix is to enable the interface named in the method’s signature — here Element. Errors of the form “cannot find type HtmlInputElement in crate web_sys” mean the type’s own feature is missing. Unstable APIs — WebGPU, some media APIs — additionally require the web_sys_unstable_apis cfg flag:

# .cargo/config.toml
[build]
rustflags = ["--cfg=web_sys_unstable_apis"]

Step 4 — measure the effect on compile time and size

Each enabled feature adds generated bindings that the compiler must process. A crate with ten features compiles web-sys in seconds; enabling hundreds — for example by copying a large feature list from an example — can add a minute to clean builds. The effect on the final .wasm is smaller than on compile time, because unused bindings are removed by the linker and wasm-opt, but the generated JavaScript glue can grow, since wasm-bindgen emits an import shim for each method actually used. Measure clean build time and output size before and after trimming:

cargo clean -p web-sys && time cargo build --release --target wasm32-unknown-unknown
ls -l pkg/*.wasm pkg/*.js

The measurement method for size is described in analyzing Wasm size with twiggy.

A trimmed feature list versus a kitchen-sink list A trimmed list of a dozen features compiles web-sys quickly and keeps the dependency graph understandable. A kitchen-sink list copied from examples compiles hundreds of unused interfaces, slowing clean builds, with little effect on the optimised binary. trimmed (12 features) web-sys compiles in seconds list documents what the crate uses easy to review in diffs the goal kitchen sink (300+) clean builds much slower unclear which APIs are used final .wasm barely changes trim it

Step 5 — keep the list tidy over time

Feature lists accumulate: an API is removed from the code but its feature stays. Periodically remove features and rebuild — whatever still compiles was unused. For large crates, group features by purpose with comments, and split browser-API code into its own module or crate so the list belongs to the code that needs it. Libraries should enable only what they use and let applications add more; a library that enables hundreds of features forces every dependent crate to compile them. If a library offers optional browser integrations, put their web-sys features behind the library’s own Cargo features.

Organising browser code around the feature list

The feature list is a map of the crate’s contact surface with the browser, and it is worth keeping that surface small for reasons beyond build time. Code that touches web-sys can only be tested in a browser or with wasm-bindgen-test, while plain Rust logic can be tested natively with cargo test in milliseconds. A clean structure therefore puts web-sys calls in a thin platform layer — a module that reads input events, draws to the canvas, talks to storage — and keeps the core logic in pure Rust that takes and returns ordinary values. The web-sys features then belong to that one module, and a glance at it shows every browser API the application depends on. When a second platform appears — a native desktop build, a server-side renderer, a test harness — only the platform layer needs another implementation. Many Rust front-end frameworks follow this split internally, and following it in your own crate makes the feature list short almost automatically, because only a small part of the code needs browser types at all.

Alternatives when web-sys does not fit

Occasionally web-sys is not the right tool. A brand-new API may not be in its WebIDL yet, or may be only partially described. A third-party library’s JavaScript API is not in web-sys at all. And for a handful of calls in a size-critical module, the generated bindings can be heavier than needed. In those cases, write your own extern "C" declarations with #[wasm_bindgen], exactly as web-sys does internally, covering only the methods you call. That is also a quick way to try an experimental API before it lands in web-sys — the declaration is a few lines, with js_name and method attributes to match the JavaScript shape. js-sys remains the place for JavaScript built-ins (Array, Promise, Reflect, Date) and needs no features. And gloo, a set of higher-level crates built on web-sys, wraps common tasks such as timers, events and storage with idiomatic Rust APIs while enabling the necessary features internally, which can be simpler than managing the raw bindings for routine work.

Expected output

The crate builds with a feature list of a dozen entries, each matching an API the code calls; a clean build of web-sys takes a few seconds; and removing any single feature produces a compile error pointing at the code that needs it.

Gotchas

  • Method missing despite the interface being enabled. A type in its signature is not enabled. Add that feature too.
  • dyn_into failing to compile. The target type’s feature is missing.
  • Unstable APIs not found. They need --cfg=web_sys_unstable_apis in addition to the feature.
  • Libraries enabling everything. Dependents pay the compile time. Keep library feature lists minimal.
  • Copying feature lists from examples. They include far more than you use. Start empty and add as needed.

Performance note

Clean-building web-sys with 12 features took 4.1 s on a laptop; with 340 features copied from a large example it took 71 s. The optimised .wasm differed by under 1 KB, and the JavaScript glue by about 3 KB, because only used bindings generate code.

Clean build time of web-sys by feature count Seconds to compile the web-sys crate from clean with twelve, sixty and three hundred forty features enabled, on the same laptop. seconds to compile web-sys 12 features 4.1 s 60 features 13.8 s 340 features 71 s

Frequently Asked Questions

Is there a tool that computes the features automatically? Some community tools scan code for web-sys types, but the compiler errors plus docs.rs notes are usually enough.

Do features affect js-sys? No. js-sys covers JavaScript built-ins and has no per-interface features.

Why does enabling a feature not increase the binary size? Unused bindings are dead code and are removed during linking and optimisation.

Can two crates enable different web-sys features? Yes — Cargo unifies features, so the final build has the union of everything any crate enabled.

Where do event types like MouseEvent come from? They are web-sys interfaces too, each with its own feature, needed when you cast an Event to a specific type.

Should features be listed alphabetically? Grouping by purpose with comments — DOM, canvas, events, storage — reads better, and makes it obvious which part of the crate needs each one.

← Back to wasm-bindgen Deep Dive