Skip to content

Commit 256da76

Browse files
committed
refactor: add a new crossRealm option for performance
1 parent 8dcd38d commit 256da76

File tree

9 files changed

+104
-85
lines changed

9 files changed

+104
-85
lines changed

.changeset/four-beds-visit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"stable-hash-x": minor
3+
---
4+
5+
refactor: add a new `crossRealm` option for performance

.size-limit.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[
22
{
33
"path": "./lib/index.js",
4-
"limit": "540B"
4+
"limit": "600B"
55
}
66
]

README.md

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
[![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
1414
[![changesets](https://img.shields.io/badge/maintained%20with-changesets-176de3.svg)](https://github.com/changesets/changesets)
1515

16-
A tiny and fast (540b <sup>[unpkg](https://unpkg.com/stable-hash-x@latest/lib/index.js)</sup>) lib for "stably hashing" a JavaScript value, works with cross-realm objects. Originally created for [SWR](https://github.com/vercel/swr) by [Shu Ding][] at [`stable-hash`](https://github.com/shuding/stable-hash), we forked it because the original one is a bit out of maintenance for a long time.
16+
A tiny and fast (600b <sup>[unpkg](https://unpkg.com/stable-hash-x@latest/lib/index.js)</sup>) lib for "stably hashing" a JavaScript value, works with cross-realm objects. Originally created for [SWR](https://github.com/vercel/swr) by [Shu Ding][] at [`stable-hash`](https://github.com/shuding/stable-hash), we forked it because the original one is a bit out of maintenance for a long time.
1717

1818
It's similar to `JSON.stringify(value)`, but:
1919

@@ -31,6 +31,7 @@ It's similar to `JSON.stringify(value)`, but:
3131
- [Array](#array)
3232
- [Object](#object)
3333
- [`Function`, `Class`, `Set`, `Map`, `Buffer`...](#function-class-set-map-buffer)
34+
- [Cross-realm](#cross-realm)
3435
- [Benchmark](#benchmark)
3536
- [Notes](#notes)
3637
- [Sponsors and Backers](#sponsors-and-backers)
@@ -49,6 +50,8 @@ yarn add stable-hash-x
4950
import { hash } from 'stable-hash-x'
5051

5152
hash(anyJavaScriptValueHere) // returns a string
53+
54+
hash(anyJavaScriptValueHere, true) // if you're running in cross-realm environment, it's disabled by default for performance
5255
```
5356

5457
## Examples
@@ -153,37 +156,39 @@ hash(foo) === hash(foo)
153156
hash(foo) !== hash(new Set([1]))
154157
```
155158

159+
### Cross-realm
160+
161+
```js
162+
import { runInNewContext } from 'node:vm'
163+
164+
const obj1 = {
165+
a: 1,
166+
b: new Date('2022-06-25T01:55:27.743Z'),
167+
c: /test/,
168+
f: Symbol('test'),
169+
}
170+
const obj2 = runInNewContext(`({
171+
a: 1,
172+
b: new Date('2022-06-25T01:55:27.743Z'),
173+
c: /test/,
174+
f: Symbol('test'),
175+
})`)
176+
177+
obj1 === obj2 // false
178+
hash(obj1) === hash(obj2) // true
179+
```
180+
156181
## Benchmark
157182

158183
```log
159-
clk: ~2.91 GHz
160-
cpu: Apple M1 Max
161-
runtime: node 22.16.0 (arm64-darwin)
162-
163-
benchmark avg (min … max) p75 / p99 (min … top 1%)
164-
------------------------------------------- -------------------------------
165-
stable-hash-x 7.87 µs/iter 7.38 µs █
166-
(6.67 µs … 749.13 µs) 11.42 µs ▇█▃
167-
(104.00 b … 859.30 kb) 10.89 kb ▁███▅▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁
168-
4.41 ipc ( 1.81% stalls) 98.08% L1 data cache
169-
28.04k cycles 123.52k instructions 29.75% retired LD/ST ( 36.75k)
170-
171-
hash-object 15.07 µs/iter 14.95 µs █ █
172-
(14.77 µs … 16.93 µs) 15.00 µs ▅ ▅ ▅▅█ ▅█▅ ▅
173-
(659.78 b … 3.26 kb) 1.95 kb █▁▁█▁▁▁▁▁▁███▁▁███▁▁█
174-
4.97 ipc ( 1.22% stalls) 99.33% L1 data cache
175-
46.36k cycles 230.44k instructions 35.12% retired LD/ST ( 80.94k)
176-
177-
json-stringify-deterministic 8.37 µs/iter 8.41 µs █
178-
(8.29 µs … 8.50 µs) 8.44 µs █ █
179-
( 1.65 kb … 1.65 kb) 1.65 kb █▁████▁██▁█▁▁▁█▁█▁███
180-
5.17 ipc ( 1.28% stalls) 99.40% L1 data cache
181-
25.99k cycles 134.30k instructions 35.51% retired LD/ST ( 47.69k)
182-
183-
summary
184-
stable-hash-x
185-
1.06x faster than json-stringify-deterministic
186-
1.91x faster than hash-object
184+
┌─────────┬────────────────────────────────┬──────────────────┬───────────────────┬────────────────────────┬────────────────────────┬─────────┐
185+
│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │
186+
├─────────┼────────────────────────────────┼──────────────────┼───────────────────┼────────────────────────┼────────────────────────┼─────────┤
187+
│ 0 │ 'stable-hash-x' │ '7790.5 ± 1.63%' │ '6917.0 ± 166.00' │ '141091 ± 0.05%' │ '144571 ± 3388' │ 128362 │
188+
│ 1 │ 'hash-object' │ '17673 ± 0.69%' │ '16583 ± 292.00' │ '59046 ± 0.07%' │ '60303 ± 1043' │ 56583 │
189+
│ 2 │ 'json-stringify-deterministic' │ '10842 ± 0.86%' │ '10167 ± 250.00' │ '96508 ± 0.05%' │ '98357 ± 2361' │ 92235 │
190+
│ 3 │ 'stable-hash' │ '7801.1 ± 1.68%' │ '6958.0 ± 167.00' │ '141110 ± 0.05%' │ '143719 ± 3534' │ 128187 │
191+
└─────────┴────────────────────────────────┴──────────────────┴───────────────────┴────────────────────────┴────────────────────────┴─────────┘
187192
```
188193

189194
## Notes

benchmark.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { encodeUrl } from 'ab64'
66
import { flattie } from 'flattie'
77
import hashObject from 'hash-object'
88
import stringify from 'json-stringify-deterministic'
9-
import { bench, run, summary } from 'mitata'
9+
import { stableHash } from 'stable-hash'
10+
import { Bench } from 'tinybench'
1011

11-
import { hash } from 'stable-hash-x'
12+
import { stableHash as hash } from './lib/index.js'
1213

1314
// this is an example of payload
1415
const payload = {
@@ -42,7 +43,7 @@ const payload = {
4243

4344
/**
4445
* Benchmarking `stable-hash-x` vs. `hash-object` vs.
45-
* `json-stringify-deterministic`
46+
* `json-stringify-deterministic` vs. `stable-hash`
4647
*
4748
* The goal is to represent a real use-case. Because that:
4849
*
@@ -87,12 +88,26 @@ const getHashThree = obj =>
8788
.digest('base64'),
8889
)
8990

90-
summary(() => {
91-
bench('stable-hash-x', () => getHashOne(payload)).baseline()
91+
/**
92+
* @param {unknown} obj
93+
* @returns {string} The hash
94+
*/
95+
const getHashFour = obj =>
96+
encodeUrl(
97+
crypto
98+
.createHash('sha512')
99+
.update(stableHash(flattie(obj)))
100+
.digest('base64'),
101+
)
102+
103+
const bench = new Bench()
92104

93-
bench('hash-object', () => getHashTwo(payload))
105+
bench
106+
.add('stable-hash-x', () => getHashOne(payload))
107+
.add('hash-object', () => getHashTwo(payload))
108+
.add('json-stringify-deterministic', () => getHashThree(payload))
109+
.add('stable-hash', () => getHashFour(payload))
94110

95-
bench('json-stringify-deterministic', () => getHashThree(payload))
96-
})
111+
await bench.run()
97112

98-
await run()
113+
console.table(bench.table())

benchmark.txt

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,8 @@
1-
clk: ~2.91 GHz
2-
cpu: Apple M1 Max
3-
runtime: node 22.16.0 (arm64-darwin)
4-
5-
benchmark avg (min … max) p75 / p99 (min … top 1%)
6-
------------------------------------------- -------------------------------
7-
stable-hash-x 7.87 µs/iter 7.38 µs █
8-
(6.67 µs … 749.13 µs) 11.42 µs ▇█▃
9-
(104.00 b … 859.30 kb) 10.89 kb ▁███▅▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁
10-
4.41 ipc ( 1.81% stalls) 98.08% L1 data cache
11-
28.04k cycles 123.52k instructions 29.75% retired LD/ST ( 36.75k)
12-
13-
hash-object 15.07 µs/iter 14.95 µs █ █
14-
(14.77 µs … 16.93 µs) 15.00 µs ▅ ▅ ▅▅█ ▅█▅ ▅
15-
(659.78 b … 3.26 kb) 1.95 kb █▁▁█▁▁▁▁▁▁███▁▁███▁▁█
16-
4.97 ipc ( 1.22% stalls) 99.33% L1 data cache
17-
46.36k cycles 230.44k instructions 35.12% retired LD/ST ( 80.94k)
18-
19-
json-stringify-deterministic 8.37 µs/iter 8.41 µs █
20-
(8.29 µs … 8.50 µs) 8.44 µs █ █
21-
( 1.65 kb … 1.65 kb) 1.65 kb █▁████▁██▁█▁▁▁█▁█▁███
22-
5.17 ipc ( 1.28% stalls) 99.40% L1 data cache
23-
25.99k cycles 134.30k instructions 35.51% retired LD/ST ( 47.69k)
24-
25-
summary
26-
stable-hash-x
27-
1.06x faster than json-stringify-deterministic
28-
1.91x faster than hash-object
1+
┌─────────┬────────────────────────────────┬──────────────────┬───────────────────┬────────────────────────┬────────────────────────┬─────────┐
2+
│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │
3+
├─────────┼────────────────────────────────┼──────────────────┼───────────────────┼────────────────────────┼────────────────────────┼─────────┤
4+
│ 0 │ 'stable-hash-x' │ '7790.5 ± 1.63%' │ '6917.0 ± 166.00' │ '141091 ± 0.05%' │ '144571 ± 3388' │ 128362 │
5+
│ 1 │ 'hash-object' │ '17673 ± 0.69%' │ '16583 ± 292.00' │ '59046 ± 0.07%' │ '60303 ± 1043' │ 56583 │
6+
│ 2 │ 'json-stringify-deterministic' │ '10842 ± 0.86%' │ '10167 ± 250.00' │ '96508 ± 0.05%' │ '98357 ± 2361' │ 92235 │
7+
│ 3 │ 'stable-hash' │ '7801.1 ± 1.68%' │ '6958.0 ± 167.00' │ '141110 ± 0.05%' │ '143719 ± 3534' │ 128187 │
8+
└─────────┴────────────────────────────────┴──────────────────┴───────────────────┴────────────────────────┴────────────────────────┴─────────┘

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"lib"
3535
],
3636
"scripts": {
37-
"benchmark": "sudo env NO_COLOR=1 node benchmark > benchmark.txt",
37+
"benchmark": "node benchmark > benchmark.txt",
3838
"build": "tsdown -d lib -f cjs,esm",
3939
"dev": "vitest",
4040
"docs": "vite",
@@ -73,7 +73,6 @@
7373
"github-markdown-css": "^5.8.1",
7474
"hash-object": "^5.0.1",
7575
"json-stringify-deterministic": "^1.0.12",
76-
"mitata": "^1.0.34",
7776
"nano-staged": "^0.8.0",
7877
"npm-run-all2": "^8.0.4",
7978
"prettier": "^3.5.3",
@@ -84,6 +83,8 @@
8483
"simple-git-hooks": "^2.13.0",
8584
"size-limit": "^11.2.0",
8685
"size-limit-preset-node-lib": "^0.4.0",
86+
"stable-hash": "^0.0.6",
87+
"tinybench": "^4.0.1",
8788
"tsdown": "^0.12.6",
8889
"type-coverage": "^2.29.7",
8990
"typescript": "^5.8.3",

src/index.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,16 @@ const isPlainObject = (val: object) => {
3131
// This is not a serialization function, and the result is not guaranteed to be
3232
// parsable.
3333
// eslint-disable-next-line sonarjs/cognitive-complexity
34-
export function stableHash(arg: unknown): string {
34+
export function stableHash(arg: unknown, crossRealm?: boolean): string {
3535
const type = typeof arg
36-
const isDate = isType(arg, 'Date')
36+
const constructor = arg?.constructor
37+
const isDate = crossRealm ? isType(arg, 'Date') : constructor === Date
3738

38-
if (Object(arg) === arg && !isDate && !isType(arg, 'RegExp')) {
39+
if (
40+
Object(arg) === arg &&
41+
!isDate &&
42+
!(crossRealm ? isType(arg, 'RegExp') : constructor === RegExp)
43+
) {
3944
const arg_ = arg as object
4045
// Object/function, not null/date/regexp. Use WeakMap to store the id first.
4146
// If it's already hashed, directly return the result.
@@ -49,15 +54,15 @@ export function stableHash(arg: unknown): string {
4954
result = ++counter + '~'
5055
table.set(arg_, result)
5156
let index: number | string | undefined
52-
if (Array.isArray(arg)) {
57+
if (crossRealm ? Array.isArray(arg) : constructor === Array) {
5358
const arg_ = arg as unknown[]
5459
// Array.
5560
result = '@'
5661
for (index = 0; index < arg_.length; index++) {
57-
result += stableHash(arg_[index]) + ','
62+
result += stableHash(arg_[index], crossRealm) + ','
5863
}
5964
table.set(arg_, result)
60-
} else if (isPlainObject(arg_)) {
65+
} else if (crossRealm ? isPlainObject(arg_) : constructor === Object) {
6166
// Object, sort keys.
6267
result = '#'
6368
// eslint-disable-next-line sonarjs/no-alphabetical-sort
@@ -66,7 +71,7 @@ export function stableHash(arg: unknown): string {
6671
const index_ = index as keyof typeof arg_
6772
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
6873
if (arg_[index_] !== undefined) {
69-
result += index + ':' + stableHash(arg_[index_]) + ','
74+
result += index + ':' + stableHash(arg_[index_], crossRealm) + ','
7075
}
7176
}
7277
table.set(arg_, result)

tests/unit.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,6 @@ describe(`The Func-y Bunch featuring The Referential Squad`, () => {
379379
f: Symbol('test'),
380380
})`) as typeof obj1
381381
expect(obj1).not.toEqual(obj2)
382-
expect(hash(obj1)).toEqual(hash(obj2))
382+
expect(hash(obj1)).toEqual(hash(obj2, true))
383383
})
384384
})

yarn.lock

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10130,13 +10130,6 @@ __metadata:
1013010130
languageName: node
1013110131
linkType: hard
1013210132

10133-
"mitata@npm:^1.0.34":
10134-
version: 1.0.34
10135-
resolution: "mitata@npm:1.0.34"
10136-
checksum: 10c0/a78a0dd18203e47f444915e64e900e9686d5b6c53c461105f79a6b4795983a2f1fa573ce6fea697ebb29c31ff31b2e7e4f0ce072e0611e0313ebef274a0a5811
10137-
languageName: node
10138-
linkType: hard
10139-
1014010133
"mkdirp@npm:^0.5.1":
1014110134
version: 0.5.6
1014210135
resolution: "mkdirp@npm:0.5.6"
@@ -13113,7 +13106,6 @@ __metadata:
1311313106
github-markdown-css: "npm:^5.8.1"
1311413107
hash-object: "npm:^5.0.1"
1311513108
json-stringify-deterministic: "npm:^1.0.12"
13116-
mitata: "npm:^1.0.34"
1311713109
nano-staged: "npm:^0.8.0"
1311813110
npm-run-all2: "npm:^8.0.4"
1311913111
prettier: "npm:^3.5.3"
@@ -13124,6 +13116,8 @@ __metadata:
1312413116
simple-git-hooks: "npm:^2.13.0"
1312513117
size-limit: "npm:^11.2.0"
1312613118
size-limit-preset-node-lib: "npm:^0.4.0"
13119+
stable-hash: "npm:^0.0.6"
13120+
tinybench: "npm:^4.0.1"
1312713121
tsdown: "npm:^0.12.6"
1312813122
type-coverage: "npm:^2.29.7"
1312913123
typescript: "npm:^5.8.3"
@@ -13140,6 +13134,13 @@ __metadata:
1314013134
languageName: node
1314113135
linkType: hard
1314213136

13137+
"stable-hash@npm:^0.0.6":
13138+
version: 0.0.6
13139+
resolution: "stable-hash@npm:0.0.6"
13140+
checksum: 10c0/5dfd823931b789daf60d9eecbed284dafba60640b92f606c9f13e8c28e3c823d3dc595b419f2df169265ab6a107384f6d0625648be12f3b17f4d6175d9690896
13141+
languageName: node
13142+
linkType: hard
13143+
1314313144
"stackback@npm:0.0.2":
1314413145
version: 0.0.2
1314513146
resolution: "stackback@npm:0.0.2"
@@ -13447,6 +13448,13 @@ __metadata:
1344713448
languageName: node
1344813449
linkType: hard
1344913450

13451+
"tinybench@npm:^4.0.1":
13452+
version: 4.0.1
13453+
resolution: "tinybench@npm:4.0.1"
13454+
checksum: 10c0/42ed8abf2eb914cfbff2cede5fa98cca7dcaf135ab5342a3701d2f90ed3d29fc1a2dcc092a91b1dc483d0de1fa69feac3fea64fad2ddef284bba34391bc96405
13455+
languageName: node
13456+
linkType: hard
13457+
1345013458
"tinyexec@npm:^0.3.2":
1345113459
version: 0.3.2
1345213460
resolution: "tinyexec@npm:0.3.2"

0 commit comments

Comments
 (0)