Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Implement isDeepEqual manually #1686

Merged
merged 4 commits into from
Jan 24, 2025
Merged

Conversation

rafaeelaudibert
Copy link
Member

We were using a built-in Node function, but that was silly because Node functions aren't available in the browser. Most bundlers could probably work that out, but Vite doesn't, so let's implement that ourselves and avoid requiring a bundler to sort this out.

Closes #1685

We were using a builtin Node function, but that was silly because Node
functions aren't available in the browser. Most builder could probably
work that out, but Vite doesn't, so let's implement that ourselves and
avoid requiring a bundler to sort this out.

Closes #1685
@rafaeelaudibert rafaeelaudibert added the bump patch Bump patch version when this PR gets merged label Jan 24, 2025
@rafaeelaudibert rafaeelaudibert requested a review from a team January 24, 2025 03:52
Copy link

vercel bot commented Jan 24, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
posthog-js ✅ Ready (Inspect) Visit Preview Jan 24, 2025 4:02pm

Copy link

github-actions bot commented Jan 24, 2025

Size Change: 0 B

Total Size: 3.28 MB

ℹ️ View Unchanged
Filename Size
dist/all-external-dependencies.js 209 kB
dist/array.full.es5.js 266 kB
dist/array.full.js 370 kB
dist/array.full.no-external.js 369 kB
dist/array.js 182 kB
dist/array.no-external.js 180 kB
dist/customizations.full.js 13.8 kB
dist/dead-clicks-autocapture.js 14.4 kB
dist/exception-autocapture.js 9.48 kB
dist/external-scripts-loader.js 2.64 kB
dist/main.js 182 kB
dist/module.full.js 370 kB
dist/module.full.no-external.js 369 kB
dist/module.js 182 kB
dist/module.no-external.js 180 kB
dist/recorder-v2.js 117 kB
dist/recorder.js 117 kB
dist/surveys-preview.js 67.5 kB
dist/surveys.js 64.2 kB
dist/tracing-headers.js 1.76 kB
dist/web-vitals.js 10.4 kB

compressed-size-action

{
files: ['react/**/*.ts'],
rules: {
'posthog-js/no-direct-null-check': 'off',
Copy link
Member

Choose a reason for hiding this comment

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

why turn this off instead of using isNull?

Copy link
Member

@robbie-c robbie-c Jan 24, 2025

Choose a reason for hiding this comment

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

Side note, but @pauldambra what's the background on us using isNull etc rather than ===? Browser compatibility? Bytes?

Copy link
Member

@pauldambra pauldambra Jan 24, 2025

Choose a reason for hiding this comment

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

the bundle reduced by 1% when we switched to isNull and isUndefined compared to the straight comparison

ofc likely that the react stuff is being bundled and run through a minifier by someone else but consistency is probably better than varying

Copy link
Member

@pauldambra pauldambra Jan 24, 2025

Choose a reason for hiding this comment

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

we use the checks a lot and...

the minified code has (something like) fe(x) at each call site

whereas the direct check is (something like) x === void 0 when minified

so once you call isUndefined more times than the character count of adding the declaration to the bundle you add roughly half as many characters by moving to a function

it's the same reason we have some global things as variables in the utils folder because then they'll minify and we have xY in the bundle not navigator

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the context! So if someone was experimenting with a new minifier, this is something they could change to see if it made a difference.

(I'd expect a good minifier to make this kind of change automatically but I probably am hoping for too much)

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, in my mind I did that to avoid crossing the boundary from posthog-js/react to posthog-js. I now see that doesn't make sense because this is a single package for those who're installing it, so I'm happy to change :)

Copy link
Member

Choose a reason for hiding this comment

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

Ah.. fair.. yep, something to bear in my mind if/when we start splitting this up

Copy link
Member

Choose a reason for hiding this comment

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

So if someone was experimenting with a new minifier, this is something they could change to see if it made a difference.

that'd be a very good minifier... it's more of a compiler at that point 🤯

Copy link
Member

@pauldambra pauldambra Jan 24, 2025

Choose a reason for hiding this comment

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

there's two sources of (avoidable) size in the repo IMO

  • unnecessary repetition of primitives - like this example
  • heavy use of classes and no minification of class fields/properties

@@ -0,0 +1,19 @@
export function isDeepEqual(obj1: any, obj2: any): boolean {
Copy link
Member

Choose a reason for hiding this comment

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

What's the provenance of this? Is it directly copied from the node library? If you wrote or modified it, it needs tests.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've attempted to write this function a few times and always been wrong, so would be interested in the answer to this!

Copy link
Member

Choose a reason for hiding this comment

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

we've had to handle recursive json in other places in the SDK too... which is why i wondered about json stringify comparison performance

const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)

if (keys1.length !== keys2.length) return false
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (keys1.length !== keys2.length) return false
if (keys1.length !== keys2.length) {
return false
}

We generally prefer this style in the codebase. (I'm surprised prettier didn't change this!)

Comment on lines +14 to +15
if (!keys2.includes(key)) return false
if (!isDeepEqual(obj1[key], obj2[key])) return false
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (!keys2.includes(key)) return false
if (!isDeepEqual(obj1[key], obj2[key])) return false
if (!keys2.includes(key) || !isDeepEqual(obj1[key], obj2[key])) {
return false
}

{
files: ['react/**/*.ts'],
rules: {
'posthog-js/no-direct-null-check': 'off',
Copy link
Member

@robbie-c robbie-c Jan 24, 2025

Choose a reason for hiding this comment

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

Side note, but @pauldambra what's the background on us using isNull etc rather than ===? Browser compatibility? Bytes?

Copy link
Contributor

@joshsny joshsny left a comment

Choose a reason for hiding this comment

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

Looks good, we need to be very careful with that deep equal function

react/src/utils/object-utils.ts Show resolved Hide resolved
@@ -0,0 +1,19 @@
export function isDeepEqual(obj1: any, obj2: any): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

I've attempted to write this function a few times and always been wrong, so would be interested in the answer to this!

@abhyuditjain
Copy link

How about this:

export function isDeepEqual(obj1: any, obj2: any, visited = new WeakMap()): boolean {
    if (obj1 === obj2) return true;

    if (obj1 === null || typeof obj1 !== 'object' || obj2 === null || typeof obj2 !== 'object') {
        return false;
    }

    // check visited to avoid infinite recursion
    if (visited.has(obj1) && visited.get(obj1) === obj2) return true;
    visited.set(obj1, obj2);

    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);

    if (keys1.length !== keys2.length) return false;

    // set for O(1) lookups
    const keys2Set = new Set(keys2);

    for (const key of keys1) {
        if (!keys2Set.has(key) || !isDeepEqual(obj1[key], obj2[key], visited)) {
            return false;
        }
    }

    return true;
}

Avoids keys2.includes() for each key and has a visited set for circular references.

@rafaeelaudibert rafaeelaudibert merged commit 607a185 into main Jan 24, 2025
28 checks passed
@rafaeelaudibert rafaeelaudibert deleted the fix-strict-deep-equal branch January 24, 2025 16:09
@rafaeelaudibert
Copy link
Member Author

We'll have a follow-up PR moving this new helper function to the PosthogProvider file. It's good enough for our use-case - nobody should have huge/circular configs, so this is fine, but this function can't be used whenever we need a general deepCompare.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bump patch Bump patch version when this PR gets merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

v1.209.0 causing build failures with ES Build
5 participants