mirror: A fast canonicalisation utility for stable object equality
0

Configure Feed

Select the types of activity you want to include in your feed.

docs: highlight benchmark winner

Marais Rossouw (Jun 30, 2026, 5:46 PM +1000) 2b0b4743 fbb5c5c2

+89 -122
+2 -6
lib/mod.bench.ts
··· 98 98 lib: () => import('npm:json-stringify-deterministic@^1.0'), 99 99 run: (m, o) => m.default(o, { cycles: true }), 100 100 }, 101 - 'json-stable-stringify-without-jsonify': { 102 - lib: () => import('npm:json-stable-stringify-without-jsonify@^1.0'), 103 - run: (m, o) => m.default(o), 104 - }, 105 101 'json-sorted-stringify': { 106 102 lib: () => import('npm:json-sorted-stringify@^1.0'), 107 103 run: (m, o) => m.default(o), ··· 143 139 fn: (ctx) => { 144 140 let o = value(); 145 141 ctx.start(); 146 - let _ = c.run(mods[name], o) 142 + let _ = c.run(mods[name], o); 147 143 ctx.end(); 148 - } 144 + }, 149 145 }); 150 146 } 151 147 }
+71 -86
readme.md
··· 62 62 <!-- BEGIN BENCHMARK --> 63 63 64 64 ``` 65 - ✔ simple ~ 1.1µs @ 870,271 ops/sec ± 0.22% 66 - ✔ deep ~ 8.4µs @ 118,807 ops/sec ± 0.01% 67 - ✔ deep circular ~ 6.0µs @ 166,470 ops/sec ± 0.51% 68 - ✔ big ~ 82.1µs @ 12,175 ops/sec ± 1.06% 69 - ✔ leafy ~ 23.8µs @ 41,957 ops/sec ± 0.70% 65 + ✔ simple 829,712 ops/sec 66 + ✔ deep 117,578 ops/sec 67 + ✔ deep circular 169,652 ops/sec 68 + ✔ big 11,664 ops/sec 69 + ✔ leafy 41,925 ops/sec 70 70 ``` 71 71 72 72 <details><summary>All candidates</summary> 73 73 74 - ``` 74 + ```diff 75 75 simple 76 - ✔ object-identity ~ 1.1µs @ 870,271 ops/sec ± 0.22% 77 - ✔ safe-stable-stringify ~ 1.8µs @ 563,781 ops/sec ± 0.46% 78 - ✔ ohash ~ 3.8µs @ 262,901 ops/sec ± 0.25% 79 - ✔ object-hash ~ 10.2µs @ 98,164 ops/sec ± 1.84% 80 - ✔ hash-it † ~ 3.0µs @ 335,235 ops/sec ± 2.05% 81 - ✔ json-stable-stringify ~ 3.1µs @ 317,679 ops/sec ± 0.30% 82 - ✔ fast-json-stable-stringify ~ 2.1µs @ 471,606 ops/sec ± 0.22% 83 - ✔ tiny-stable-stringify ~ 2.5µs @ 403,418 ops/sec ± 1.49% 84 - ✔ json-stringify-deterministic ~ 3.2µs @ 313,455 ops/sec ± 0.63% 85 - ✔ json-stable-stringify-without-jsonify ~ 3.2µs @ 312,450 ops/sec ± 0.59% 86 - ✔ json-sorted-stringify ~ 2.4µs @ 411,299 ops/sec ± 0.86% 87 - ✔ canonicalize ~ 3.7µs @ 271,025 ops/sec ± 0.17% 88 - ✔ swr/_internal/stableHash ~ 3.9µs @ 255,223 ops/sec ± 0.60% 89 - ✘ @tufjs/canonical-json 90 - ────────────────────────────────────────────────────────────────────────── 91 - ⭐︎ object-identity (1.54x faster than safe-stable-stringify) 76 + + ✔ object-identity 829,712 ops/sec 77 + ✔ safe-stable-stringify 544,561 ops/sec 1.52x 78 + ✔ ohash 257,343 ops/sec 3.22x 79 + ✔ object-hash 96,890 ops/sec 8.56x 80 + ✔ hash-it † 328,222 ops/sec 2.53x 81 + ✔ json-stable-stringify 309,313 ops/sec 2.68x 82 + ✔ fast-json-stable-stringify 458,943 ops/sec 1.81x 83 + ✔ tiny-stable-stringify 402,523 ops/sec 2.06x 84 + ✔ json-stringify-deterministic 311,265 ops/sec 2.67x 85 + ✔ json-sorted-stringify 402,961 ops/sec 2.06x 86 + ✔ canonicalize 267,754 ops/sec 3.10x 87 + ✔ swr/_internal/stableHash 250,439 ops/sec 3.31x 88 + ✘ @tufjs/canonical-json 92 89 93 90 deep 94 - ✔ object-identity ~ 8.4µs @ 118,807 ops/sec ± 0.01% 95 - ✔ safe-stable-stringify ~ 13.3µs @ 75,024 ops/sec ± 0.66% 96 - ✔ ohash ~ 26.0µs @ 38,497 ops/sec ± 0.57% 97 - ✔ object-hash ~ 56.2µs @ 17,807 ops/sec ± 0.95% 98 - ✔ hash-it † ~ 22.8µs @ 43,812 ops/sec ± 0.14% 99 - ✔ json-stable-stringify ~ 24.1µs @ 41,487 ops/sec ± 0.95% 100 - ✔ fast-json-stable-stringify ~ 14.2µs @ 70,522 ops/sec ± 0.39% 101 - ✔ tiny-stable-stringify ~ 16.1µs @ 62,039 ops/sec ± 0.56% 102 - ✔ json-stringify-deterministic ~ 23.2µs @ 43,102 ops/sec ± 0.15% 103 - ✔ json-stable-stringify-without-jsonify ~ 23.3µs @ 42,858 ops/sec ± 0.18% 104 - ✔ json-sorted-stringify ~ 15.8µs @ 63,287 ops/sec ± 0.73% 105 - ✔ canonicalize ~ 26.8µs @ 37,383 ops/sec ± 0.16% 106 - ✔ swr/_internal/stableHash ~ 29.0µs @ 34,460 ops/sec ± 22.90% 107 - ✔ @tufjs/canonical-json ~ 34.9µs @ 28,625 ops/sec ± 0.17% 108 - ─────────────────────────────────────────────────────────────────────────── 109 - ⭐︎ object-identity (1.58x faster than safe-stable-stringify) 91 + + ✔ object-identity 117,578 ops/sec 92 + ✔ safe-stable-stringify 74,688 ops/sec 1.57x 93 + ✔ ohash 38,360 ops/sec 3.07x 94 + ✔ object-hash 17,877 ops/sec 6.58x 95 + ✔ hash-it † 44,260 ops/sec 2.66x 96 + ✔ json-stable-stringify 41,899 ops/sec 2.81x 97 + ✔ fast-json-stable-stringify 70,452 ops/sec 1.67x 98 + ✔ tiny-stable-stringify 62,336 ops/sec 1.89x 99 + ✔ json-stringify-deterministic 43,571 ops/sec 2.70x 100 + ✔ json-sorted-stringify 62,523 ops/sec 1.88x 101 + ✔ canonicalize 37,471 ops/sec 3.14x 102 + ✔ swr/_internal/stableHash 34,445 ops/sec 3.41x 103 + ✔ @tufjs/canonical-json 28,960 ops/sec 4.06x 110 104 111 105 deep circular 112 - ✔ object-identity ~ 6.0µs @ 166,470 ops/sec ± 0.51% 113 - ✔ safe-stable-stringify ~ 7.5µs @ 132,624 ops/sec ± 0.27% 114 - ✔ ohash ~ 17.2µs @ 58,116 ops/sec ± 0.49% 115 - ✔ object-hash ~ 45.7µs @ 21,863 ops/sec ± 0.07% 116 - ✔ hash-it † ~ 16.1µs @ 61,985 ops/sec ± 0.31% 117 - ✘ json-stable-stringify 118 - ✔ fast-json-stable-stringify ~ 10.2µs @ 97,666 ops/sec ± 0.51% 119 - ✘ tiny-stable-stringify 120 - ✔ json-stringify-deterministic ~ 15.5µs @ 64,604 ops/sec ± 0.41% 121 - ✘ json-stable-stringify-without-jsonify 122 - ✘ json-sorted-stringify 123 - ✘ canonicalize 124 - ✔ swr/_internal/stableHash ~ 15.4µs @ 65,066 ops/sec ± 21.11% 125 - ✘ @tufjs/canonical-json 126 - ─────────────────────────────────────────────────────────────────────────── 127 - ⭐︎ object-identity (1.26x faster than safe-stable-stringify) 106 + + ✔ object-identity 169,652 ops/sec 107 + ✔ safe-stable-stringify 133,794 ops/sec 1.27x 108 + ✔ ohash 58,065 ops/sec 2.92x 109 + ✔ object-hash 22,029 ops/sec 7.70x 110 + ✔ hash-it † 62,480 ops/sec 2.72x 111 + ✘ json-stable-stringify 112 + ✔ fast-json-stable-stringify 94,420 ops/sec 1.80x 113 + ✘ tiny-stable-stringify 114 + ✔ json-stringify-deterministic 62,684 ops/sec 2.71x 115 + ✘ json-sorted-stringify 116 + ✘ canonicalize 117 + ✔ swr/_internal/stableHash 54,642 ops/sec 3.10x 118 + ✘ @tufjs/canonical-json 128 119 129 120 big 130 - ✔ object-identity ~ 82.1µs @ 12,175 ops/sec ± 1.06% 131 - ✔ safe-stable-stringify ~ 140.5µs @ 7,118 ops/sec ± 0.04% 132 - ✔ ohash ~ 304.4µs @ 3,285 ops/sec ± 0.53% 133 - ✔ object-hash ~ 513.2µs @ 1,949 ops/sec ± 1.23% 134 - ✔ hash-it † ~ 276.5µs @ 3,617 ops/sec ± 0.61% 135 - ✔ json-stable-stringify ~ 228.2µs @ 4,382 ops/sec ± 1.06% 136 - ✔ fast-json-stable-stringify ~ 164.2µs @ 6,091 ops/sec ± 0.56% 137 - ✔ tiny-stable-stringify ~ 180.0µs @ 5,556 ops/sec ± 1.08% 138 - ✔ json-stringify-deterministic ~ 224.9µs @ 4,447 ops/sec ± 1.01% 139 - ✔ json-stable-stringify-without-jsonify ~ 224.1µs @ 4,463 ops/sec ± 0.66% 140 - ✔ json-sorted-stringify ~ 172.5µs @ 5,797 ops/sec ± 0.21% 141 - ✔ canonicalize ~ 262.7µs @ 3,807 ops/sec ± 1.01% 142 - ✔ swr/_internal/stableHash ~ 287.7µs @ 3,476 ops/sec ± 16.90% 143 - ✔ @tufjs/canonical-json ~ 387.7µs @ 2,580 ops/sec ± 0.32% 144 - ─────────────────────────────────────────────────────────────────────────── 145 - ⭐︎ object-identity (1.71x faster than safe-stable-stringify) 121 + + ✔ object-identity 11,664 ops/sec 122 + ✔ safe-stable-stringify 7,097 ops/sec 1.64x 123 + ✔ ohash 3,287 ops/sec 3.55x 124 + ✔ object-hash 1,940 ops/sec 6.01x 125 + ✔ hash-it † 3,583 ops/sec 3.26x 126 + ✔ json-stable-stringify 4,396 ops/sec 2.65x 127 + ✔ fast-json-stable-stringify 6,219 ops/sec 1.88x 128 + ✔ tiny-stable-stringify 5,771 ops/sec 2.02x 129 + ✔ json-stringify-deterministic 4,448 ops/sec 2.62x 130 + ✔ json-sorted-stringify 5,719 ops/sec 2.04x 131 + ✔ canonicalize 3,799 ops/sec 3.07x 132 + ✔ swr/_internal/stableHash 3,536 ops/sec 3.30x 133 + ✔ @tufjs/canonical-json 2,542 ops/sec 4.59x 146 134 147 135 leafy 148 - ✔ object-identity ~ 23.8µs @ 41,957 ops/sec ± 0.70% 149 - ✔ safe-stable-stringify ~ 50.3µs @ 19,896 ops/sec ± 0.47% 150 - ✔ ohash ~ 30.9µs @ 32,336 ops/sec ± 0.38% 151 - ✔ object-hash ~ 87.7µs @ 11,397 ops/sec ± 1.27% 152 - ✔ hash-it † ~ 84.3µs @ 11,860 ops/sec ± 0.97% 153 - ✔ json-stable-stringify ~ 115.1µs @ 8,686 ops/sec ± 0.87% 154 - ✔ fast-json-stable-stringify ~ 59.6µs @ 16,765 ops/sec ± 0.11% 155 - ✔ tiny-stable-stringify ~ 107.8µs @ 9,277 ops/sec ± 0.23% 156 - ✔ json-stringify-deterministic ~ 116.5µs @ 8,583 ops/sec ± 0.29% 157 - ✔ json-stable-stringify-without-jsonify ~ 117.2µs @ 8,532 ops/sec ± 0.35% 158 - ✔ json-sorted-stringify ~ 107.9µs @ 9,266 ops/sec ± 0.15% 159 - ✔ canonicalize ~ 93.3µs @ 10,720 ops/sec ± 0.50% 160 - ✔ swr/_internal/stableHash ~ 134.1µs @ 7,459 ops/sec ± 31.44% 161 - ✔ @tufjs/canonical-json ~ 217.2µs @ 4,605 ops/sec ± 1.07% 162 - ─────────────────────────────────────────────────────────────────────────── 163 - ⭐︎ object-identity (1.30x faster than ohash) 136 + + ✔ object-identity 41,925 ops/sec 137 + ✔ safe-stable-stringify 20,088 ops/sec 2.09x 138 + ✔ ohash 32,586 ops/sec 1.29x 139 + ✔ object-hash 11,693 ops/sec 3.59x 140 + ✔ hash-it † 12,092 ops/sec 3.47x 141 + ✔ json-stable-stringify 8,863 ops/sec 4.73x 142 + ✔ fast-json-stable-stringify 16,968 ops/sec 2.47x 143 + ✔ tiny-stable-stringify 9,269 ops/sec 4.52x 144 + ✔ json-stringify-deterministic 8,680 ops/sec 4.83x 145 + ✔ json-sorted-stringify 9,522 ops/sec 4.40x 146 + ✔ canonicalize 10,858 ops/sec 3.86x 147 + ✔ swr/_internal/stableHash 7,982 ops/sec 5.25x 148 + ✔ @tufjs/canonical-json 4,714 ops/sec 8.89x 164 149 ``` 165 150 166 151 </details>
+16 -30
scripts/bench.ts
··· 8 8 9 9 const report = JSON.parse(new TextDecoder().decode(stdout)); 10 10 11 - type Row = { name: string; ok: boolean; avg?: number; ops?: number; jitter?: number }; 11 + type Row = { name: string; ok: boolean; ops?: number }; 12 12 13 13 const groups: string[] = []; 14 14 const order: string[] = []; // candidate union, in first-seen (registration) order ··· 25 25 metric[b.group][b.name] = { 26 26 name: b.name, 27 27 ok: true, 28 - avg: ok.avg, // nanoseconds 29 28 ops: Math.round(1e9 / ok.avg), 30 - // no stddev, so approximate jitter from p75 vs avg. 31 - jitter: (Math.abs(ok.p75 - ok.avg) / ok.avg) * 100, 32 29 }; 33 30 } 34 31 ··· 38 35 39 36 const nf = new Intl.NumberFormat('en-AU'); 40 37 41 - function dur(ns: number): string { 42 - if (ns < 1e3) return `${ns.toFixed(1)}ns`; 43 - if (ns < 1e6) return `${(ns / 1e3).toFixed(1)}µs`; 44 - return `${(ns / 1e6).toFixed(1)}ms`; 45 - } 46 - 47 - function line(r: Row, label: string, nameW: number, avgW: number, opsW: number): string { 38 + function line(r: Row, label: string, nameW: number, opsW: number, rel = ''): string { 48 39 const mark = r.ok ? '✔' : '✘'; 49 40 if (r.ops == null) return `${mark} ${label}`; 50 - return `${mark} ${label.padEnd(nameW)} ~ ${dur(r.avg!).padStart(avgW)} @ ${ 51 - nf 52 - .format(r.ops) 53 - .padStart(opsW) 54 - } ops/sec ± ${r.jitter!.toFixed(2)}%`; 41 + const tail = rel.trim() ? ` ${rel}` : ''; 42 + return `${mark} ${label.padEnd(nameW)} ${nf.format(r.ops).padStart(opsW)} ops/sec${tail}`; 55 43 } 56 44 57 - const avgW = (rs: Row[]) => Math.max(0, ...rs.map((r) => (r.avg != null ? dur(r.avg).length : 0))); 58 45 const opsW = (rs: Row[]) => 59 46 Math.max(0, ...rs.map((r) => (r.ops != null ? nf.format(r.ops).length : 0))); 60 47 61 48 // Top: just this library, one row per shape (the readme set). 62 49 const baseline = groups.map((g) => rows[g].find((x) => x.name === BASELINE)!); 63 50 const sumNameW = Math.max(...groups.map((g) => g.length)); 64 - const sumAvgW = avgW(baseline); 65 51 const sumOpsW = opsW(baseline); 66 - const summary = groups.map((g, k) => line(baseline[k], g, sumNameW, sumAvgW, sumOpsW)).join('\n'); 52 + const summary = groups.map((g, k) => line(baseline[k], g, sumNameW, sumOpsW)).join('\n'); 67 53 68 - // Details: every candidate, grouped by shape, with the winner called out. 69 54 const details = groups 70 55 .map((g) => { 71 56 const nameW = Math.max(...rows[g].map((r) => r.name.length)); 72 - const w = [nameW, avgW(rows[g]), opsW(rows[g])] as const; 73 - const lines = rows[g].map((r) => line(r, r.name, ...w)); 57 + const w = [nameW, opsW(rows[g])] as const; 74 58 75 - // Fastest candidate wins; note how far ahead of the next best it is. 76 - const ranked = rows[g].filter((r) => r.ops != null).sort((a, b) => b.ops! - a.ops!); 77 - const [win, alt] = ranked; 78 - if (win) { 79 - const ahead = alt ? ` (${(win.ops! / alt.ops!).toFixed(2)}x faster than ${alt.name})` : ''; 80 - lines.push('─'.repeat(Math.max(...lines.map((l) => l.length))), `⭐︎ ${win.name}${ahead}`); 81 - } 59 + const base = rows[g].find((r) => r.name === BASELINE); 60 + const rel = (r: Row) => 61 + base?.ops && r.ops && r.name !== BASELINE ? `${(base.ops / r.ops).toFixed(2)}x` : ''; 62 + const relW = Math.max(0, ...rows[g].map((r) => rel(r).length)); 63 + 64 + const lines = rows[g].map((r) => { 65 + const body = line(r, r.name, ...w, rel(r).padStart(relW)); 66 + return r.name === BASELINE ? `+ ${body}` : ` ${body}`; 67 + }); 82 68 83 69 return `${g}\n${lines.join('\n')}`; 84 70 }) ··· 92 78 '', 93 79 '<details><summary>All candidates</summary>', 94 80 '', 95 - '```', 81 + '```diff', 96 82 details, 97 83 '```', 98 84 '',