I did some extra digging for a deepEquals library and found dequal. Compared to es-toolkit/isEqual it yields an even smaller build (1kb vs 3kb before gzip, after the ratio is similar-ish). ๐งต
borkdude@m1-5 /tmp $ npx esbuild --bundle --minify --format=esm --target=es2020 --outfile=out.js <<< "import { isEqual } from 'es-toolkit'; console.log(isEqual({}, {}))";
gzip -c out.js | wc -c
out.js 3.1kb
โก Done in 25ms
1259
borkdude@m1-5 /tmp $ npm install dequal
added 1 package, and audited 3 packages in 504ms
found 0 vulnerabilities
borkdude@m1-5 /tmp $ npx esbuild --bundle --minify --format=esm --target=es2020 --outfile=out.js <<< "import { dequal } from 'dequal'; console.log(dequal({}, {}))";
gzip -c out.js | wc -c
out.js 1.0kb
โก Done in 11ms
503
I also did a benchmark for performance:
import { dequal } from 'dequal';
import { isEqual } from 'es-toolkit';
import Benchmark from 'benchmark';
// Test data
const obj1 = {
name: 'Alice',
age: 30,
hobbies: ['reading', 'cycling', 'hiking'],
meta: { active: true, score: 42 },
nested: Array.from({ length: 1000 }, (_, i) => ({ id: i, v: Math.random() })),
};
const obj2 = structuredClone(obj1);
const suite = new Benchmark.Suite();
suite
.add('dequal', () => {
dequal(obj1, obj2);
})
.add('es-toolkit/isEqual', () => {
isEqual(obj1, obj2);
})
.on('cycle', (event) => {
console.log(String(event.target));
})
.on('complete', function () {
console.log('\nFastest is ' + this.filter('fastest').map('name'));
})
.run({ async: true });
$ node bench.mjs
dequal x 13,219 ops/sec ยฑ0.20% (95 runs sampled)
es-toolkit/isEqual x 3,045 ops/sec ยฑ0.32% (98 runs sampled)
Fastest is dequal
My thoughts: perhaps we can switch eucalypt to dequal. And if it works well, we could maybe even adopt dequal for = in squint for structural equality...
cc @chris358All for it!
isEqual:
../../docs/index.html 42.78 kB โ gzip: 15.16 kB
../../docs/small.html 27.97 kB โ gzip: 9.76 kB
../../docs/games.html 34.64 kB โ gzip: 12.06 kB
dequal:
../../docs/index.html 40.59 kB โ gzip: 14.43 kB
../../docs/small.html 25.78 kB โ gzip: 9.01 kB
../../docs/games.html 35.70 kB โ gzip: 12.43 kB
Works as a drop-in replacement. All tests pass.Since dequal says it can compare functions there is a complicated bit of the code that may be able to be replaced where it has to assign functions IDs to see if they changed. I wonder if I can just get rid of that now. ๐ค
Once I replaced the isEquals in the games.html demos:
../../docs/games.html 32.45 kB โ gzip: 11.33 kBok, a minor decrease
function comparison: that's weird, I don't think it should work outside of just object identity
yeah, it's just doing object identity: https://github.com/lukeed/dequal/blob/37c21f675c1f538f2d4b63ebf19a161411e7a5fd/test/index.js#L171-L173
Yeah the function comparison looks like a dead end. I think the minor decrease is worth switching to dequals. I also like that it is a standalone library not part of some larger thing.
so if I would inline dequal into squint and switch = to it, you could then just use = from squint. I wonder if this is a good idea.
my brain then went to the next step: I could make a HashMap type in Squint that derives from Map and uses hashing + collision solution using dequal
also Set
I will make a PR to squint just to see how it feels. It would bloat "minimal" apps with this impl though.
unless you avoid = with identical?
You mean update Eucalypt to use identical?
no!
I meant "unless one avoided"
for small stuff that doesn't need deep equals
oh here is a gotcha:
- keys within Maps use value equality(I don't think it would matter for eucalypt, just thinking more broadly)
Oh wait, "value" is good, he means value vs reference equals
perfect
ok...
$ ./node_cli.js --repl --show -e "(= {:a 1} {:a 1} {:a 1})"
(async function() { var squint_core = await import('squint-cljs/core.js');
globalThis.user = globalThis.user || {};
return squint_core._EQ_(({ "a": 1 }), ({ "a": 1 }), ({ "a": 1 })) })()
truehttps://github.com/squint-cljs/squint/pull/716
I'm still pondering about the consequences. Being able to use this in case would be nice too but I'd need to switch up case to use _EQ_ etc as well.
But I guess I can do this step by step...
$ ./node_cli.js --show -e "(= {:a 1} {:a 1} {:a 1})" | esbuild --minify --bundle | gzip | wc -c
836433 bytes difference gzipped when you don't use =
1135 bytes ungzipped
all tests are passing which is promising
need to fix stuff like this too:
$ ./node_cli.js --repl --show -e "(= [1 2 3] '(1 2 3))"
(async function() { var squint_core = await import('squint-cljs/core.js');
globalThis.user = globalThis.user || {};
return squint_core._EQ_([1, 2, 3], squint_core.list(1, 2, 3)) })()
falseI think I can reduce the size of dequals a bit by ripping out regexp, arraybuffer, etc which aren't supported in CLJS as equal anyway
also Date isn't supported in CLJS as equals
Implemented a test that also tests eucalypt for breakage locally and switched over eucalypt to squint = :
https://github.com/squint-cljs/squint/pull/716
merged. https://github.com/squint-cljs/squint/releases/tag/v0.8.156 I'll bump eucalypt
Merged, thanks!