JS ↔ WASM boundary

wasm-bindgen makes calls easy; the expensive part is often data movement and allocation—not Rust’s inner loops.

Overview

Browser events arrive on the JS side. Each crossing into WASM may copy linear-memory buffers or convert strings. A naive “serialize whole document to Rust on every input” design will bottleneck before your diff algorithm matters.

Copy cost & encoding

  • UTF-16 vs UTF-8: JS exposes strings as UTF-16 code units; Rust String indices differ. Pass byte offsets or scalar offsets explicitly across the boundary, or normalize in one place.
  • Large strings: Full HTML serialization from JS to Rust duplicates memory. Prefer incremental patches, hashed snapshots, or shared views where your security model allows.

Batching & op streams

Send small operations (insert/delete/format) when possible instead of shipping the whole doc. For collaboration, binary CRDT updates are often smaller than text snapshots.

Debouncing helps for expensive validation—not for losing IME ordering; never batch in ways that reorder composition relative to commits.

Async vs synchronous host calls

Promises from WASM resolve on microtasks; input handlers may expect synchronous DOM updates. If Rust work is async, guard against selection races: the user may type again before your WASM task finishes.

FFI hygiene

  • Prefer stable handles for long-lived documents (e.g. boxed Rust state in WASM linear memory).
  • Expose narrow APIs: “apply op” vs “replace entire tree”.
  • Measure with browser Performance panel: time in WASM vs glue.