mirror: A fast canonicalisation utility for stable object equality
0

Configure Feed

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

feat: perf golf :tada:

Marais Rossouw (Jun 24, 2026, 12:09 PM +1000) e4ea2ad7 23088bff

+55 -76
+44 -64
lib/mod.ts
··· 1 - let seen = new WeakMap<object, string>(); 2 - 3 - function walk(input: any, ref_index: number) { 4 - if (input == null || typeof input !== 'object') return String(input); 5 - 6 - let tmp: any; 7 - let out = ''; 8 - let i = 0; 9 - let type = Object.prototype.toString.call(input); 10 - if ( 11 - !(type === '[object RegExp]' || type === '[object Date]') && 12 - seen.has(input) 13 - ) { 14 - return seen.get(input)!; 15 - } 16 - seen.set(input, '~' + ++ref_index); 17 - 18 - switch (type) { 19 - case '[object Set]': 20 - tmp = Array.from(input as Set<unknown>); 21 - case '[object Array]': 22 - { 23 - tmp ||= input; 24 - out += 'a'; 25 - for (; i < tmp.length; out += walk(tmp[i++], ref_index)); 26 - } 27 - break; 1 + function walk(input: any, seen: any[], depth: number): string { 2 + if (input == null || typeof input !== 'object') return '' + input; 28 3 29 - case '[object Object]': 30 - { 31 - out += 'o'; 32 - tmp = Object.keys(input).sort(); 33 - for ( 34 - ; 35 - i < tmp.length; 36 - out += tmp[i] + walk(input[tmp[i++]], ref_index) 37 - ); 38 - } 39 - break; 40 - 41 - case '[object Map]': 42 - { 43 - out += 'o'; 44 - tmp = Array.from((input as Map<string, unknown>).keys()).sort(); 45 - for ( 46 - ; 47 - i < tmp.length; 48 - out += tmp[i] + walk(input.get(tmp[i++]), ref_index) 49 - ); 50 - } 51 - break; 4 + if (input instanceof Date) return 'd' + +input; 5 + if (input instanceof RegExp) return 'r' + input.source + input.flags; 52 6 53 - case '[object Date]': 54 - return 'd' + +input; 7 + let ref: any = seen.indexOf(input); 8 + if (~ref) return (ref = seen[ref + 1]) > 0 ? '~' + ref : ref; 9 + ref = seen.push(input, ++depth) - 1; 55 10 56 - case '[object RegExp]': 57 - return 'r' + input.source + input.flags; 11 + let out: string, i = 0, keys: any; 58 12 59 - default: 60 - throw new Error(`Unsupported value ${input}`); 13 + if (Array.isArray(input)) { 14 + for (out = 'a'; i < input.length; out += walk(input[i++], seen, depth)); 15 + } else if (input instanceof Set) { 16 + out = 'a'; 17 + for (let value of input) out += walk(value, seen, depth); 18 + } else if (input instanceof Map) { 19 + out = 'o'; 20 + if (input.size > 1) { 21 + for ( 22 + keys = [...input.keys()].sort(); 23 + i < keys.length; 24 + out += keys[i] + walk(input.get(keys[i++]), seen, depth) 25 + ); 26 + } else { 27 + for (keys of input) out += keys[0] + walk(keys[1], seen, depth); 28 + } 29 + } // Plain objects, class instances and null-prototype objects have no 30 + // `Symbol.toStringTag`; exotic builtins (Promise, typed arrays, WeakMap, 31 + // ArrayBuffer, etc) do, so this both selects key-walkable objects and 32 + // rejects the unsupported ones below. 33 + else if (input[Symbol.toStringTag] === undefined) { 34 + out = 'o'; 35 + keys = Object.keys(input); 36 + if (keys.length === 1) { 37 + out += keys[0] + walk(input[keys[0]], seen, depth); 38 + } else { 39 + if (keys.length > 1) keys.sort(); 40 + for (; i < keys.length; out += keys[i] + walk(input[keys[i++]], seen, depth)); 41 + } 42 + } else { 43 + throw new Error('Unsupported value'); 61 44 } 62 45 63 - seen.set(input, out); 46 + seen[ref] = out; 64 47 return out; 65 48 } 66 49 67 50 /** 68 - * Creates a shape equivalent identifier for an input object. 69 - * This is useful for comparing objects, where keys could be provided in any order. 51 + * Canonicalize a value into a stable identity string. Two structurally-equal 52 + * inputs return the same id, regardless of key order. 70 53 * 71 54 * @example 72 55 * ```ts 73 - * const obj = { a: 1, b: 2 }; 74 - * const obj2 = { b: 2, a: 1 }; 75 - * 76 - * console.log(identify(obj) === identify(obj2)); // true 56 + * identify({ a: 1, b: 2 }) === identify({ b: 2, a: 1 }); // true 77 57 * ``` 78 58 */ 79 59 export function identify<T>(input: T): string { 80 - return walk(input, 0); 60 + return walk(input, [], 0); 81 61 }
+11 -12
readme.md
··· 52 52 ## 💨 Benchmark 53 53 54 54 ``` 55 - benchmark time (avg) iter/s (min … max) p75 p99 p995 56 - --------------------------------------------------------------------------- ----------------------------- 57 - object-identity 2.2 µs/iter 453,803.6 (1.99 µs … 2.44 µs) 2.35 µs 2.44 µs 2.44 µs 58 - object-hash 8.76 µs/iter 114,168.3 (7.96 µs … 225.33 µs) 8.71 µs 11.75 µs 14.92 µs 59 - json-stable-stringify 1.77 µs/iter 565,184.5 (1.75 µs … 1.86 µs) 1.77 µs 1.86 µs 1.86 µs 60 - tiny-stable-stringify 1.63 µs/iter 612,009.4 (1.62 µs … 1.68 µs) 1.64 µs 1.68 µs 1.68 µs 55 + | benchmark | time/iter (avg) | iter/s | (min … max) | p75 | p99 | p995 | 56 + | ----------------------- | --------------- | ------------- | --------------------- | -------- | -------- | -------- | 57 + | object-identity | 573.0 ns | 1,745,000 | (540.1 ns … 607.1 ns) | 585.2 ns | 607.1 ns | 607.1 ns | 58 + | object-hash | 6.7 µs | 149,000 | ( 6.2 µs … 164.9 µs) | 6.5 µs | 8.0 µs | 21.2 µs | 59 + | json-stable-stringify | 1.7 µs | 588,400 | ( 1.6 µs … 2.9 µs) | 1.6 µs | 2.9 µs | 2.9 µs | 60 + | tiny-stable-stringify | 1.6 µs | 639,600 | ( 1.5 µs … 1.6 µs) | 1.6 µs | 1.6 µs | 1.6 µs | 61 61 62 62 summary 63 63 object-identity 64 - 1.35x slower than tiny-stable-stringify 65 - 1.25x slower than json-stable-stringify 66 - 3.97x faster than object-hash 64 + 2.73x faster than tiny-stable-stringify 65 + 2.97x faster than json-stable-stringify 66 + 11.71x faster than object-hash 67 67 ``` 68 68 69 - > ^ `object-identity` is not as feature-full it's alternatives, specifically around `function` 70 - > values and other node builtins. So take this benchmark with a grain of salt, as it's only testing 71 - > "json-like" payloads. 69 + > ^ `object-identity` only handles JSON-like data by design. It won't fingerprint functions or every 70 + > Node builtin the way some alternatives do, so these numbers only reflect the payloads it targets. 72 71 73 72 ## License 74 73