squint 2025-10-06

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 @chris358

๐Ÿ’ฏ 2
๐Ÿ˜ 3

All 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 kB

ok, a minor decrease

function comparison: that's weird, I don't think it should work outside of just object identity

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.

๐Ÿ‘€ 1

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

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?

I meant "unless one avoided"

for small stuff that doesn't need deep equals

๐Ÿ‘ 1

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

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 })) })()
true

https://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
     836

433 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)) })()
false

I 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, thanks!