Performance
Benchmarks and scaling characteristics.
Performance
All benchmarks run on Apple M3 Pro using mitata. Numbers will vary on your hardware -- run them yourself to get a feel.
Operations
| Operation | Time | Throughput |
|---|---|---|
| has() | ~2ns | 500M ops/sec |
| hasMigration() | ~45ns | 22M ops/sec |
| validate() | ~108ns | 9.3M ops/sec |
| explain() | ~385ns | 2.6M ops/sec |
| transform (1 hop) | ~505ns | 2.0M ops/sec |
| transform (10 hops) | ~1.8us | 560K ops/sec |
| transform (99 hops) | ~13.9us | 72K ops/sec |
| findPath (100 nodes) | ~7.5us | 133K ops/sec |
Lookups are O(1) regardless of registry size. Transforms scale linearly with chain depth at roughly ~140ns per hop. The graph is built once at registry creation, so path resolution doesn't rebuild anything.
Tradeoffs
Every feature has a cost. Here's what each one adds.
| Operation | Time | Throughput |
|---|---|---|
| Hooks (noop callbacks) | +19% | 519ns -> 620ns |
| Deprecated migration | +25% | 443ns -> 553ns |
| Pipe builder vs bare fn | ~2x | 484ns -> 903ns |
| Context calls (3w + 3d) | ~1.7x | 485ns -> 815ns |
Hooks with empty callbacks add about 19% -- the timing hooks (onTransform, onStep) add performance.now() calls and per-step object allocations. When no timing hooks are registered, doba skips all of that entirely. The pipe builder is about 2x slower than a hand-written migration function because it runs each step (rename, drop, add, map) as a separate object spread. If that matters in your hot path, use bare functions. For most cases the ergonomics are worth it.
Providing an explicit path skips BFS entirely and saves about 52% on longer chains (2.0us vs 4.2us at 25 hops). Worth doing if you're calling the same transform repeatedly and already know the route.
Scaling
| Dimension | Scaling | Notes |
|---|---|---|
| Chain depth | Linear | ~140ns per additional hop |
| Schema count | O(1) | ~2ns regardless of registry size |
| Object size | Constant | passes references, not copies |
| Batch size | Linear | ~430ns per item |
| Graph complexity | BFS | 10k+ edges |
Edge cases
| Operation | Time | Throughput |
|---|---|---|
| Same-schema (validate: none) | ~120ns | identity, no work |
| Same-schema (validate: end) | ~194ns | validates only |
| Unknown schema (error) | ~103ns | fails immediately |
| No path found (error) | ~668ns | runs BFS, finds nothing |
| Bidirectional migration | ~501ns | same cost as one-way |
Errors fail fast. An unknown schema returns in ~103ns without touching the graph. A missing path is slower (~668ns) because BFS has to exhaust the search before giving up.
Recursive structures
These measure the cost of migrating complex nested data through a single transform. The time is dominated by the migration function itself, not doba's overhead.
| Operation | Time | Throughput |
|---|---|---|
| Tree (364 nodes) | ~762ns | 1.3M ops/sec |
| Linked list (1k nodes) | ~14.4us | 69K ops/sec |
| Nested object (100 levels) | ~9.2us | 109K ops/sec |
| Graph node (1k edges) | ~5.7us | 175K ops/sec |
Run it yourself
bun run bench # core operations
bun tests/features.bench.ts # feature overhead, explain, pipe, hooks
bun tests/stress.bench.ts # extreme scale, recursive structures