Skip to content

v2: performance, correctness, and documentation overhaul#345

Merged
xobotyi merged 8 commits intomasterfrom
v2-dev
Feb 28, 2026
Merged

v2: performance, correctness, and documentation overhaul#345
xobotyi merged 8 commits intomasterfrom
v2-dev

Conversation

@xobotyi
Copy link
Member

@xobotyi xobotyi commented Feb 25, 2026

Summary

  • Lazy WeakMap — circular reference tracking is now lazily allocated; leaf type comparisons (Date, RegExp, Set, TypedArray, ArrayBuffer) have zero overhead (3d074fd)
  • TypedArray/DataView correctness — comparisons now respect byteOffset and byteLength, comparing only the viewed slice instead of the full underlying buffer (daa5e2d)
  • SharedArrayBuffer support — previously fell through to plain object comparison and silently returned wrong results (953ba56)
  • valueOf/toString hardening — requires matching function references before comparing by value; fixes boxed NaN inconsistency (4f71a6f)
  • Switch dispatch — constructor checks replaced with switch statements for better branch prediction (069afbd)
  • Early key count check — object property count compared before iterating keys (183f29d)
  • README rewrite — new "Comparison Behavior" section documenting per-type semantics and design decisions; updated Performance and Supported Types sections (85b2158, 5649c5f)

Breaking Changes

  • valueOf/toString comparison now requires both instances to share the same function reference. Objects with different arrow function implementations for valueOf now fall through to property comparison instead of comparing by valueOf() result.
  • TypedArray views into the same buffer but different regions are now correctly identified as different (previously compared the full buffer).

Benchmark (vs master)

Biggest improvements on leaf types from lazy WeakMap + closure removal:

Category Improvement
ArrayBuffers +27-28%
Sets (inequal) +26%
Arrays +17-24%
Dates +20-22%
RegExps +20%
Maps +6-10%
Objects/Mixed +2-7%

Eliminates two per-call allocations: closure creation and WeakMap
instantiation. The comparison function is now a static module-level
function with visited state passed as a parameter.

WeakMap is lazily initialized on first use — non-recursive comparisons
(primitives, Date, RegExp, Set, TypedArray) never allocate it.

Benchmarks show 8-22% improvement across all comparison types, with
leaf types (Date, RegExp) benefiting most.
Replaces DataView.getUint8() byte-by-byte iteration with Uint8Array
indexed access, which also improves performance on binary comparisons.

BREAKING CHANGE: TypedArray and DataView subviews now compare only the
bytes within their byteOffset/byteLength range, not the full underlying
ArrayBuffer. Previously, two views into different regions of the same
buffer would incorrectly compare as equal.
Converts sequential identity comparisons on constructor into two switch
statements. V8 can optimize switch on reference identity into a jump
table, reducing per-type dispatch cost.

Benchmarks show Date comparison improved from 1.52x to 1.11x vs dequal.
Other types show modest gains from reduced branching.
Previously SharedArrayBuffer fell through to plain object comparison,
returning true for any two instances regardless of size or content.

Now handled via the same byte-level Uint8Array comparison used for ArrayBuffer.
The valueOf/toString comparison path now verifies that both objects share the
same function reference before comparing results. Previously, any object with
a non-default valueOf was compared by its return value, even when the other
object had a different valueOf implementation.

Also fixes boxed NaN inconsistency: isEqual(Object(NaN), Object(NaN)) now
returns true, consistent with isEqual(NaN, NaN). The valueOf comparison uses
the same self-comparison NaN check as the primitive path.
Previously the key count comparison happened after the property loop,
meaning objects where b had extra keys required a full iteration of a
before discovering the mismatch. Moving the check before the loop exits
immediately on key count difference.
Add symbol-keyed properties to Gotchas as an intentional omission.
Update custom classes entry to reflect the valueOf/toString
implementation-matching requirement added in the previous commit.
Add detailed Comparison Behavior section explaining how each data type is
compared and the design rationale behind key decisions:

- Sets/Maps: reference equality follows SameValueZero identity model
- Symbols: excluded as hidden metadata, not data-carrying properties
- Custom classes: valueOf/toString terminal comparison semantics
- TypedArrays: byte-level comparison preserving NaN bit patterns
- Circular references: tracked via lazily allocated WeakMap

Update Performance section to reflect lazy allocation optimization.
Add missing supported types: SharedArrayBuffer, DataView, boxed primitives.
Sync CLAUDE.md gotchas and metadata with README changes.
@xobotyi xobotyi requested a review from Copilot February 25, 2026 12:59
@xobotyi xobotyi self-assigned this Feb 25, 2026
@xobotyi xobotyi added the enhancement New feature or request label Feb 25, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR delivers a comprehensive v2 overhaul focused on performance, correctness, and documentation. The core implementation moves from a closure-based approach to a module-level recursive function with lazy WeakMap allocation, achieving significant performance gains (17-28% for leaf types) by avoiding unnecessary allocations for non-recursive comparisons.

Changes:

  • Lazy WeakMap allocation eliminates overhead for leaf types (Date, RegExp, Set, TypedArrays, ArrayBuffers) while maintaining circular reference detection for recursive structures
  • TypedArray/DataView comparisons now correctly respect byteOffset and byteLength, comparing only the viewed slice instead of the full buffer
  • SharedArrayBuffer support added (previously fell through to incorrect object comparison)
  • valueOf/toString hardening requires matching function references, fixing boxed NaN edge cases and preventing false equality between objects with different arrow function implementations
  • Comprehensive documentation of comparison semantics, design rationale, and per-type behavior

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/is-equal.ts Core refactor: lazy WeakMap, switch-based dispatch, corrected TypedArray/DataView/SharedArrayBuffer handling, hardened valueOf/toString checks, early key count optimization
src/is-equal.test.ts New tests for boxed NaN handling and valueOf/toString function reference requirements
src/fixtures/tests.ts Updated valueOf/toString expectations to reflect breaking change; added comprehensive TypedArray subview, DataView subview, and SharedArrayBuffer test suites
README.md New "Comparison Behavior" section documenting per-type semantics and design decisions; updated Performance section explaining lazy allocation strategy; expanded Supported Types list
CLAUDE.md Updated implementation notes, gotchas section, and git commit configuration guidance

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@xobotyi xobotyi merged commit 4c6372c into master Feb 28, 2026
10 checks passed
@xobotyi xobotyi deleted the v2-dev branch February 28, 2026 16:55
@github-actions
Copy link
Contributor

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants