Skip to content

Improve object keys performance - #3130

Open
joshkel wants to merge 1 commit into
hapijs:masterfrom
joshkel:keys-performance
Open

Improve object keys performance#3130
joshkel wants to merge 1 commit into
hapijs:masterfrom
joshkel:keys-performance

Conversation

@joshkel

@joshkel joshkel commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

I noticed that constructing Joi.object schemas of complex object types was causing significant slowdown in my Node.js application startup. Profiling revealed that a major source of slowdown was the topographical sorting within object keys' rebuild: sort was called for each added property, resulting in N * N topographical sorts.

To address this, this PR uses @hapi/topo's manual option to sort once, at the end. If that sort fails, it redoes the rebuild operation, sorting per key as the current behavior, so that tryWithPath can throw the more detailed error message.

The updated code constructs my application's schemas roughly 3.6x faster.

The preexisting test suite did not cover rebuild / @hapi/topo's error handling, so I added tests to cover the affected behavior.

I noticed that constructing Joi.object schemas of complex object types
was causing significant slowdown in my Node.js application startup.
Profiling revealed that a major source of slowdown was the topographical
sorting within object keys' `rebuild`: `sort` was called for each added
property, resulting in N * N topographical sorts.

To address this, this PR uses @hapi/topo's `manual` option to sort once, at
the end. If that sort fails, it redoes the rebuild operation, sorting
per key as the current behavior, so that `tryWithPath` can throw the
more detailed error message.

The updated code constructs my application's schemas roughly 3.6x
faster.

The preexisting test suite did not cover `rebuild` / @hapi/topo's error
handling, so I added tests to cover the affected behavior.
Comment thread lib/types/keys.js
const topo = new Topo.Sorter();
for (const child of schema.$_terms.keys) {
Common.tryWithPath(() => topo.add(child, { after: child.schema.$_rootReferences(), group: child.key }), child.key);
Common.tryWithPath(() => topo.add(child, { after: child.schema.$_rootReferences(), group: child.key, manual: true }), child.key);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Very nice find! Though I'd go for a less repetitive version like calling an external function:

schema.$_terms.keys = new internals.Keys(...internals.sortKeys(schema.$_terms.keys));

.....

internals.sortKeys = function (keys, manual = true) {

    const topo = new Topo.Sorter();
    for (const child of keys) {
        Common.tryWithPath(() => topo.add(child, { after: child.schema.$_rootReferences(), group: child.key, manual }), child.key);
    }

    try {
        return topo.sort();
    }
    catch {
        return internals.sortKeys(keys, false);
    }
};

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants