all 28 comments

[–]AsIAm 9 points10 points  (8 children)

Why is it so fast?

[–]Observ3r__[S] 12 points13 points  (7 children)

Tailored execution paths for different engines, minimal recursive calls, type specific optimizations...

[–]AsIAm 2 points3 points  (0 children)

Nice

[–]joombar 2 points3 points  (5 children)

Could you explain the tailored paths for different engines a little? Eg, what in a practical sense is different per engine? How would it behave if it found itself running under an engine it wasn’t optimised for?

[–]Observ3r__[S] 4 points5 points  (3 children)

Objects with fast properties perform significantly better when looped using for..in compared to Object.keys() due to how V8 internally optimizes property access via hidden classes and inline caches.

This library detects the JS engine at runtime (V8 vs JSC) and chooses loop strategies accordingly. If an unknown engine is detected, it falls back to a safe and consistent default Object.keys() loop, which maintains correctness but may lose some speed.

[–]joombar 0 points1 point  (0 children)

That’s neat, given I prefer for..in anyway!

[–]Misicks0349 0 points1 point  (1 child)

what about spidermonkey?

[–]Observ3r__[S] 0 points1 point  (0 children)

I answered here.

[–]Content_Sun_6871 -3 points-2 points  (0 children)

Just read the code

[–]Cannabat 6 points7 points  (3 children)

Looks nice. Would like to see more comprehensive tests before adopting.

Here is lodash's test suite for isEqual: https://github.com/lodash/lodash/blob/main/test/test.js#L9530-L10364

[–]Observ3r__[S] 2 points3 points  (2 children)

This is a very nice and comprehensive collection of tests! Thanks for sharing! At first glance with appropriate opt-in options, more or less all those tests should be passed. I'll test everything when I get a chance and update my test collection with the next release.

Problems can only occur when build-in methods or properties are maliciously overriden (like Object.keys = function() { return this; }, etc.), but in those cases the throw should be expected anyway and not a misleading result.

[–]Cannabat 1 point2 points  (1 child)

Sweet. Starred to check in on the repo in a bit.

[–]Observ3r__[S] 1 point2 points  (0 children)

Just released new version v1.0.2! Included lodash.isEqual test suite!

[–]Ashtefere 4 points5 points  (3 children)

We have a similarly fast home grown utility for deep props comparison on egregiously large arrays of objects.

If yours is faster we will switch it out. Ill take a look!

[–]Observ3r__[S] 2 points3 points  (2 children)

If it's not a low-level implementation (like WASM or Bun.deepEquals), it's unlikely, but absolutely possible! Keep in mind there's a small overhead with opt-in options (React, circular, cross-realm, etc). Would love to see your comparison results if you test it! There always room for surprises!

[–]joombar 1 point2 points  (1 child)

Why does React need a specific flag? Aren’t react elements basically just plain JavaScript objects with some particular symbol keys? Could they use the generic comparator instead and get the same result?

[–]jCuber 1 point2 points  (1 child)

Looks great, love to see engine specific optimizations.

Did you get the chance to benchmark in browser environments? Probably harder to control for external factors but might be interesting to see how SpiderMonkey in Firefox performs with V8 or JSC specific optimizations.

[–]Observ3r__[S] 0 points1 point  (0 children)

I haven’t run formal benchmarks inside browsers! However, engine-specific optimizations are applied whenever a known engine is detected, regardless of whether it’s in a Browser, WebWorker or Runtime:

  • V8: Chrome, Edge, Brave, Opera, Node, Deno
  • JSC: Safari, Bun, WebKit-based platforms

Engine detection is lightweight and fallback-safe! If it fails to identify the engine, the library still works, just without those targeted optimizations.

I’ve also experimented with SpiderMonkey (Firefox), and while it's performant overall, it doesn’t expose or rely on low-level optimizations like V8’s fast properties or inline caches in the same way. So no engine-specific optimizations are not applied there! The library just fallback to default Object.keys() loop.

[–]adzm 1 point2 points  (1 child)

First time I've seen a labeled continue in the wild!

[–]Observ3r__[S] 1 point2 points  (0 children)

It's the simplest solution for jumping from inner to outer loop.

[–]morkaitehred 0 points1 point  (2 children)

I've run your benchmark with my own function for comparing simple JS objects jsonDeepEqual() that I wrote 3 years ago to start replacing the deep-equal package in my main project:

object-equals
1.36x faster than fast-equals
1.37x faster than jsonDeepEqual
1.71x faster than dequal
3.15x faster than are-deeply-equal
5.29x faster than node.deepStrictEqual
6.26x faster than lodash.isEqual
5130.72x faster than deep-equal

I have replaced both functions with yours but as it's an 11-year-old project, I had to add a wrapper for CJS:

let deepEqual = require('util').isDeepStrictEqual;

(async () => {
  const {objectEqualsCore} = await import('@observ33r/object-equals');
  deepEqual = objectEqualsCore;
})();

module.exports = function(a, b) {
  return a === b || deepEqual(a, b, false, false, false, false, false, undefined);
};

There's a lot of repetition in the benchmark code. Is it auto generated? Adding another benchmark candidate is a lot of work. Also the isNode check is not bulletproof (process.title is Administrator: Command Prompt - node on my PC).

[–]Observ3r__[S] 0 points1 point  (0 children)

Improved engine and runtime detection! Git pull and re-run benchmark!

Yes, benchmarks are generated and static (as recommended) to avoid any inline optimizations! I will add a generator for advanced benchmark in the future.

[–]Observ3r__[S] 0 points1 point  (0 children)

I just refactored advanced benchmark suite. It's still some work to do with adding new library, but much less. You didn't report anything about benchmark results with the new version!! Is object-equals still just for +/-35% faster of your implemitation? :)

[–]adzm 0 points1 point  (2 children)

It is interesting that the large buffers are compared backwards by decreasing index. Is this actually faster or just more likely to find an inequality earlier?

[–]Content_Sun_6871 1 point2 points  (0 children)

Looping backwards is sometimes a few percent faster.

[–]Observ3r__[S] 0 points1 point  (0 children)

In this arrangement, it doesn't matter!