Skip to main content

TypeScript and WebAssembly Performance

Finding

The final TypeScript/Wasm facade is neutral or faster. Every shared median is between 5.12% faster and 0.49% slower; every slower p95 is at most 2.19%. Identity import/erase is 5.12% faster after removing non-Android registry state.

The audit used three paired Node.js 22.23.1 processes on a GitHub-hosted Ubuntu 24.04 x86_64 runner. Values are nanoseconds per authored package call. Negative deltas mean the current SDK is faster.

OperationBytesOld median nsCurrent median nsDeltaCurrent p95 nsp95 delta
Import and erase identity32141,684134,435-5.12%159,910-4.83%
Read cached public key010100.00%100.00%
NIP-44 encrypt32404,194403,169-0.25%438,495-1.65%
NIP-44 decrypt32379,507376,219-0.87%383,524-7.21%
NIP-44 encrypt1,024400,769402,733+0.49%476,286-0.55%
NIP-44 decrypt1,024385,368386,885+0.39%422,006+1.20%
NIP-44 encrypt64,0001,146,5481,103,895-3.72%1,228,332+2.18%
NIP-44 decrypt64,0001,177,3191,153,489-2.02%1,170,370-1.96%
Sign NIP-01 event321,562,0501,527,607-2.20%1,628,120-12.57%
Parse and verify event374350,280348,015-0.65%359,536-1.84%

The 10-ns cached getter is an API-shape check, not an absolute performance claim.

Current-only baseline

OperationBytesMedian nsp95 ns
Create NIP-59 rumor1547,95682,280
Create NIP-17 text message15113,490119,251
Create durable gift wrap153,204,8293,498,655
Authenticate and unwrap152,182,4612,249,816
Stream-encrypt attachment64,000743,412765,677

Optimizations retained

  • One package-level Wasm module is shared by CommonJS, Node ESM, browser, and Worker entry points. Consumers do not download or instantiate duplicate modules.
  • LocalIdentity imports secret bytes once, caches the public key in the authored object, and does not allocate Android-only registry state.
  • Public events, messages, metadata, and errors are frozen plain JavaScript values; generated wasm-bindgen classes stay internal.
  • Complete encryption, signing, wrapping, relay, and media operations cross Wasm in coarse calls.
  • Attachment APIs process bounded chunks instead of materializing an unbounded browser file.

Footprint

ArtifactOld bytesCurrent bytesDelta
WebAssembly module1,636,5622,860,071+74.76%
WebAssembly module, gzip -n1,291,0871,675,061+29.74%
Packed npm artifactn/a1,780,547Release-candidate baseline

The Wasm growth reflects the complete relay, chat/account, Noise, Negentropy, and media surface. The packed artifact still contains exactly one Wasm binary; its CommonJS, ESM, browser, and Worker facades reference that same file.

Developer guidance

  • Initialize the package once per JavaScript realm and reuse the resulting module. Do not instantiate Wasm for every account or message.
  • Import one identity per authenticated session, clear the caller-owned secret buffer, and call erase() only for logout or early authority removal.
  • Move bulk crypto, parsing, reconciliation, and attachment work to a dedicated Worker when it would block a browser event loop.
  • Pass complete bounded strings, events, relay frames, and byte chunks. Avoid per-field Wasm calls.
  • Retain immutable parsed values at trust boundaries rather than reparsing relay JSON for each projection.
  • Stream files and enforce application cancellation/backpressure; do not read an arbitrary Blob into one buffer.

The packed external consumer verifies CommonJS, Node ESM, browser, and Worker exports. Run performance through the manual workflow or the commands in Performance Benchmarks source guide.

Open work

Add real Chromium/WebKit/Firefox startup, memory, Worker-transfer, and mobile browser measurements before promoting Node numbers to browser release gates. Wasm size is the main JavaScript follow-up: dependency convergence or link optimization must retain every vector and rerun the packed consumers.