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.
| Operation | Bytes | Old median ns | Current median ns | Delta | Current p95 ns | p95 delta |
|---|---|---|---|---|---|---|
| Import and erase identity | 32 | 141,684 | 134,435 | -5.12% | 159,910 | -4.83% |
| Read cached public key | 0 | 10 | 10 | 0.00% | 10 | 0.00% |
| NIP-44 encrypt | 32 | 404,194 | 403,169 | -0.25% | 438,495 | -1.65% |
| NIP-44 decrypt | 32 | 379,507 | 376,219 | -0.87% | 383,524 | -7.21% |
| NIP-44 encrypt | 1,024 | 400,769 | 402,733 | +0.49% | 476,286 | -0.55% |
| NIP-44 decrypt | 1,024 | 385,368 | 386,885 | +0.39% | 422,006 | +1.20% |
| NIP-44 encrypt | 64,000 | 1,146,548 | 1,103,895 | -3.72% | 1,228,332 | +2.18% |
| NIP-44 decrypt | 64,000 | 1,177,319 | 1,153,489 | -2.02% | 1,170,370 | -1.96% |
| Sign NIP-01 event | 32 | 1,562,050 | 1,527,607 | -2.20% | 1,628,120 | -12.57% |
| Parse and verify event | 374 | 350,280 | 348,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
| Operation | Bytes | Median ns | p95 ns |
|---|---|---|---|
| Create NIP-59 rumor | 15 | 47,956 | 82,280 |
| Create NIP-17 text message | 15 | 113,490 | 119,251 |
| Create durable gift wrap | 15 | 3,204,829 | 3,498,655 |
| Authenticate and unwrap | 15 | 2,182,461 | 2,249,816 |
| Stream-encrypt attachment | 64,000 | 743,412 | 765,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.
LocalIdentityimports 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-bindgenclasses 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
| Artifact | Old bytes | Current bytes | Delta |
|---|---|---|---|
| WebAssembly module | 1,636,562 | 2,860,071 | +74.76% |
WebAssembly module, gzip -n | 1,291,087 | 1,675,061 | +29.74% |
| Packed npm artifact | n/a | 1,780,547 | Release-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
Blobinto 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.