Performance & Comparison
Why WASM for Hydra SDK?
Hydra SDK leverages WebAssembly (WASM) to overcome the limitations of traditional JavaScript-based SDKs like @cardano-sdk. Here’s why WASM is the optimal choice:
Key Advantages of WASM
- Eliminates Node.js Core Module Dependencies:
- WASM allows critical functionalities (e.g., cryptography, CBOR parsing, transaction building) to run in a sandboxed environment without relying on Node.js core modules like
crypto,stream, orfs. - This ensures compatibility across both browser and Node.js environments.
- WASM allows critical functionalities (e.g., cryptography, CBOR parsing, transaction building) to run in a sandboxed environment without relying on Node.js core modules like
- Reduces Bundle Size:
- WASM loads only the required functions, avoiding the need for polyfills like
crypto-browserifyorstream-browserify. - This results in significantly smaller bundles compared to SDKs that rely on Node.js modules.
- WASM loads only the required functions, avoiding the need for polyfills like
- Boosts Performance:
- WASM executes cryptographic operations and transaction parsing at near-native speeds, reducing the overhead of JavaScript runtime.
- Cross-Platform Compatibility:
- A single WASM binary can run seamlessly on browsers, Node.js (via WASI), and mobile platforms (via bridges).
- This eliminates the need for complex interop between CommonJS and ESM modules.
Comparison of Cardano SDK Solutions
Detailed Comparison Table
| Criteria | SDKs based on @cardano-sdk | Hydra SDK (WASM-based) |
|---|---|---|
| Browser Compatibility | Requires polyfills for Node.js core modules; some APIs (e.g., fs, net) are unavailable. | Native browser support; no polyfills needed. |
| Bundle Size | Very large (hundreds of KB to >1MB) due to polyfills. | Compact (only loads WASM + minimal JS glue code). |
| Node.js Dependency | High, with direct imports of Node.js core modules. | None; all core logic resides in WASM. |
| Interop (ESM/CJS) | Prone to errors (exports is not defined, .default is not a function). | No issues; pure ESM JavaScript. |
| Crypto/CBOR Performance | Moderate (pure JS, dependent on low-performance polyfills). | High (WASM near-native speed). |
| Cross-Platform Support | Good for Node.js, limited for browsers. | Excellent; works on browsers, Node.js, and mobile. |
| Tree-Shaking | Limited, as Node.js imports are scattered, leading to unused code in bundles. | Excellent; only required WASM functions are bundled. |
| Maintenance & Scalability | Complex, as it requires tracking Node.js API changes and polyfills. | Easy to maintain; core logic is implemented in Rust/C++ and exposed via WASM. |
Dependency Flow Diagrams
Issues with SDKs based on @cardano-sdk
graph TD
A[App Browser] --> B[SDKs based on @cardano-sdk]
B --> C[Node Core Modules]
C -->|Requires Polyfill| D[crypto-browserify, stream-browserify, buffer, util...]
D -->|Increases Bundle Size| E[Runtime Errors without Polyfill]
C -->|Cannot Polyfill| F[fs, net -> Unusable Features]
Hydra SDK (WASM-based)
graph TD
A[App Browser] --> B[Hydra SDK]
B --> C["WASM Core (Rust/C++)"]
C --> D[WebAssembly Runtime]
D -->|Native Browser API| E[Crypto, CBOR, Tx Builder]
D -->|Node.js WASI| F[Crypto, CBOR, Tx Builder]
Conclusion
Hydra SDK’s WASM-based architecture provides a robust, efficient, and cross-platform solution for modern Cardano DApps. By eliminating Node.js dependencies, reducing bundle size, and boosting performance, it addresses the limitations of SDKs based on @cardano-sdk.
In-Memory Snapshot Cache (v1.3.0+)
@hydra-sdk/bridge adds a two-level in-memory cache that is rebuilt once per snapshot event and read O(1) per request.
Architecture
Hydra Node (WebSocket)
│
├── Greetings { snapshotUtxo }
│ └── updateSnapshot() ← O(n) one-time rebuild
│
└── SnapshotConfirmed { snapshot.utxo }
└── updateSnapshot() ← O(n) one-time rebuild, only if snapNum advances
GET balance / query UTxO
└── Map.get(address) ← O(1), no I/O, no WASM call
Performance Impact
| Operation | Before v1.3.0 | v1.3.0+ |
|---|---|---|
getAddressBalance() | Not available | O(1) map lookup, no I/O |
queryAddressUTxO(address) | O(n) filter over all UTxOs | O(1) index lookup |
addressesInHead() | O(n) + HTTP call | O(1) Map.keys() |
| 100 concurrent balance reads | 100 × scan + alloc | 100 × O(1) |
| Snapshot arrival | O(1) (store ref) | O(n) rebuild — once per snapshot |
Memory Usage
For a 5 000-UTxO snapshot with 2 000 unique addresses averaging 2 assets each:
addressUtxoIndex: ~200 KB (2000 Map entries)
balanceCache: ~256 KB (4000 inner Map entries)
────────────────────────────────────────────────
Total overhead: ~500 KB
Usage
import { HydraBridge } from '@hydra-sdk/bridge'
const bridge = new HydraBridge({ url: 'ws://localhost:4001' })
await bridge.connect()
// After Greetings arrives, cache is seeded automatically
const balance = bridge.getAddressBalance('addr_test1...')
if (balance === null) {
// Cold start — cache not yet seeded
} else {
const lovelace = balance.get('lovelace') ?? 0n
console.log('Balance:', Number(lovelace) / 1_000_000, 'ADA')
}
Bounded Datum Cache (@hydra-sdk/core)
convertUTxOObjectToUTxOWithOptions accepts maxDatumCacheSize to cap the number of deserialized inline datums kept in memory:
import { Converter } from '@hydra-sdk/core'
const utxos = Converter.convertUTxOObjectToUTxOWithOptions(snapshotUtxo, {
maxDatumCacheSize: 1024 // default; set to 0 to disable
})
Entries are evicted in insertion order (oldest first) when the limit is reached.
TxBuilder WASM Memory (v1.2.0+)
Every @emurgo/cardano-serialization-lib object lives in WASM linear memory and is only reclaimed when you call .free(). CSL relies on a FinalizationRegistry for automatic cleanup, but that runs non-deterministically and lags far behind a tight build loop. Before v1.2.0, building thousands of transactions in a row (e.g. spike-building inside a Hydra Head) leaked the WASM heap and made each successive build_tx() slower.
What changed
@hydra-sdk/transaction@1.2.0 frees the WASM objects it allocates deterministically:
- Every internally-allocated CSL object during
complete()is tracked and freed in afinallyblock afterbuild_tx(). The returnedTransactionis an independent struct and is unaffected; caller-supplied objects (datum / redeemer / script) are never freed. reset(),changeAddress(), andupdateProtocolParams()free the state objects they replace.dispose()(and[Symbol.dispose]) release everything the builder still holds.
Measured impact
| Metric | Before v1.2.0 | v1.2.0+ |
|---|---|---|
| WASM heap over 10,000 builds | Grows unbounded | Flat (~0 KB/iter) |
| Per-transaction build time | Degrades steadily | Steady |
| Existing test suite | — | 280 tests unchanged |
Patterns for high-volume workloads
Pick the path that frees memory for you:
import { TxBuilder } from '@hydra-sdk/transaction'
// 1. completeCbor() — you only need the bytes; the Transaction is freed for you
const cbor = await new TxBuilder()
.setInputs(utxos)
.txOut(addr, [{ unit: 'lovelace', quantity: '1000000' }])
.changeAddress(addr)
.completeCbor()
// 2. `using` — builder disposed automatically at end of scope (TS 5.2+)
{
using builder = new TxBuilder()
const bytes = await builder.setInputs(utxos)./* … */.completeCbor()
}
// 3. Reuse one builder across a spike, then dispose once
const builder = new TxBuilder()
for (const job of jobs) {
const bytes = await builder.reset().setInputs(job.utxos)./* … */.completeCbor()
}
builder.dispose()
Transaction from complete() (e.g. complete() + .to_hex()), you own it — call tx.free() when done. Under high volume, prefer completeCbor().