Footguns of the Rust WebAssembly Target

A trail in Golden, Colorado

WebAssembly—even af­ter sev­eral years of stan­dard­iza­tion—is still a nascent tech­nol­ogy.

I’ve been work­ing with Rust and WebAssembly for nearly four years now. This post is in­tended to be a dis­til­late of that ex­pe­ri­ence, for­mat­ted for de­vel­op­ers who are in­ter­ested in pub­lish­ing WebAssembly code to npm. Specif­i­cally, these are the foot­guns I’ve per­son­ally en­coun­tered while work­ing on harper.js, a WebAssembly-powered pack­age for gram­mar check­ing at the edge. This page should be valu­able if you are even spec­u­lat­ing on the pos­si­bil­ity of us­ing WebAssembly in your code­base.


1. Exposing Only Synchronous Functions

The Harper pack­age ex­poses one in­ter­face that cap­tures all of its WebAssembly in­ter­ac­tions: the Linter. This is an ob­ject that han­dles down­load­ing and com­pil­ing the Harper WebAssembly mod­ule, as well as in­vok­ing func­tions in it. Every func­tion re­turns a Promise. There are sev­eral good rea­sons for that:

  • WebAssembly mod­ules larger than 4 kilo­bytes must be in­stan­ti­ated asyn­chro­nously to avoid block­ing the event loop dur­ing down­load or com­pi­la­tion. This is a tech­ni­cal lim­i­ta­tion that can­not be avoided with clever logic.
  • If most func­tions are asyn­chro­nous, you can cen­tral­ize com­pu­ta­tion and caching into a sin­gle in­stance of the WebAssembly mod­ule, hid­ing the com­plex­ity of in­stan­ti­a­tion and mak­ing caches eas­ier to build.
  • If your prob­lem do­main can be com­pu­ta­tion­ally in­tense, it might be pru­dent to of­fload jobs onto a web worker, which is eas­ier if every func­tion of your fa­cade is asyn­chro­nous.
  • It will be eas­ier for both you and your users to ex­pose any im­por­tant func­tion as asyn­chro­nous out of the gate.

2. Assume Rust Can Trivially Do IO

Whatever you do: avoid as­sum­ing that Rust li­braries (like reqwest or rand) will be able to per­form IO with­out some work on your end. WebAssembly alone is not ca­pa­ble of IO, which means any func­tion in that cat­e­gory will re­quire some amount of JavaScript to work prop­erly.

Rather than leav­ing that up to the Rust tool­chain to fig­ure out, save your­self the headache and in­ject the nec­es­sary JavaScript func­tions di­rectly into the WebAssembly mod­ule by pass­ing them through wasm_bindgen.


3. Just Inline the WebAssembly Module

Obsidian plu­g­ins, for ex­am­ple, must be com­posed of ex­actly one JavaScript file, which means every­thing must be in­lined. In the in­ter­est of keep­ing the bun­dle size small, it’s much eas­ier for the pack­age de­vel­oper (of harper.js) to set up in­lin­ing than the plu­gin de­vel­oper.

On the other side of the spec­trum, Chrome’s Manifest V3 dis­al­lows WebAssembly from be­ing loaded in­line.

If you plan for your pack­age to be con­sumed by a va­ri­ety of ap­pli­ca­tions, know that it will also be con­sumed by a va­ri­ety of bundlers. Both bundlers and ap­pli­ca­tions are pretty in­con­sis­tent with their in­lin­ing and tree-shak­ing be­hav­ior. To avoid prob­lems, you should pro­vide two ver­sions of your pack­age:

  • One where your WebAssembly mod­ule is al­ready in­lined.
  • One where it is­n’t.

Wrapping it Up

Harper’s prob­lem do­main is not your prob­lem do­main. We have to in­te­grate with a va­ri­ety of unique ap­pli­ca­tions, which means we must keep our sys­tem flex­i­ble. That may not be the case for you, which may mean these foot­guns do not ap­ply. If you have any ques­tions about any other prob­lems the Harper pro­ject might have faced, let me know.