Skip to content

perf: Benchmarks and optimizations#1527

Open
kibertoad wants to merge 4 commits into
open-circle:mainfrom
kibertoad:perf/variant-enum-object-fastpaths
Open

perf: Benchmarks and optimizations#1527
kibertoad wants to merge 4 commits into
open-circle:mainfrom
kibertoad:perf/variant-enum-object-fastpaths

Conversation

@kibertoad

@kibertoad kibertoad commented Jun 30, 2026

Copy link
Copy Markdown

This change adds a runtime benchmark suite and three hot-path optimizations, each proven by before/after measurements. All 4486 tests pass with no behavior or type changes.

Why these paths

Profiling the schema-execution hot paths and comparing against zod showed three places where valibot did linear work that could be made constant-time, without touching its functional, tree-shakeable design or resorting to eval/JIT.

1. variant / variantAsync discriminator-map dispatch

Before, validating a discriminated union scanned the options one by one, running each option's discriminator sub-schema against the input until one matched. Cost grew with the number of options: the last option of ten was ~13x slower than the first.

Now a Map<discriminatorValue, option> is built lazily on the first run, so dispatch is a single Map.get regardless of option count. The map is disabled (falling back to the original scan) for nested variants, non-enumerable discriminators, colliding values, or NaN, so error reporting and every edge case stay identical. Result: dispatch is position-independent (up to ~13x on the worst
case, ~8x for the async schema), and even the first-option case is faster because the fast path skips the per-call closure and Set allocations of the scan.

2. enum / picklist set membership

Before, membership used Array.includes, an O(n) scan, so checking a value near the end of a large allow-list was ~3x slower than one near the start. Now a lazily-cached Set makes has O(1) and position-independent. Set.has uses the same SameValueZero comparison as Array.includes, so behavior (including NaN) is unchanged.

3. object family loop

The entry loop evaluated key in input twice per present key (once in the condition, once in the value ternary). Hoisting it to a single check gives a small, consistent win on the object happy path with no behavior change. Applied to object, strictObject, looseObject, and objectWithRest.

Design notes

  • Precomputed state (the discriminator map, the option set) is built lazily on first run, so schema creation stays cheap and tree-shaking is unaffected.
  • The fast paths only change the success-dispatch case; any ambiguity routes to the original logic, which is why the full test suite passes unchanged.

Benchmarks

  1. variant and variantAsync discriminator-map fast path (O(1) dispatch instead of scanning every option).
  2. object family: hoist the redundant key in input re-check in the entry loop.
  3. enum / picklist lazily-cached Set membership instead of Array.includes.

Baseline vs. optimized (speedup = optimized / baseline)

Benchmark Case Baseline hz Optimized hz Speedup
variant (10 options) hit first option 1,674,293 3,252,833 1.94x
variant (10 options) hit middle option 340,541 3,119,117 9.16x
variant (10 options) hit last option 236,108 3,195,553 13.54x
variant (10 options) miss 252,662 240,129 0.95x (unchanged*)
object all keys present 1,669,082 2,216,805 1.33x
object missing required key 986,683 1,259,883 1.28x
enum (200) hit first 6,662,908 7,738,684 1.16x
enum (200) hit last 2,453,115 6,979,579 2.85x
enum (200) miss 1,797,455 2,289,587 1.27x
picklist (200) hit first 6,732,057 6,836,693 1.02x
picklist (200) hit last 1,962,357 7,151,161 3.64x
picklist (200) miss 1,764,085 2,363,121 1.34x
array (100 objects) valid 91,765 102,279 1.11x (noise, untouched)
string pipe valid 5,086,026 4,827,814 0.95x (noise, untouched)

* The variant miss case is unchanged by design: a non-matching discriminator
falls through to the original slow path so the discriminator issue and message
are byte-identical to before.

Summary by CodeRabbit

  • New Features
    • Added runtime benchmark suites for arrays, objects, enums, picklists, variants (sync/async), and string pipelines.
    • Added a bench command to run the benchmark suite locally.
  • Bug Fixes
    • Improved validation performance for enum and picklist membership by caching options for faster lookups.
    • Accelerated variant and variantAsync parsing with an optional fast discriminator dispatch.
    • Reduced repeated key-presence checks during object/strict object/loose object parsing.
  • Documentation
    • Added benchmarking docs and contributor guidance emphasizing performance measurements with recorded results.

@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. enhancement New feature or request performance Its performance related labels Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

Walkthrough

The PR adds runtime performance changes in enum, picklist, object, looseObject, strictObject, objectWithRest, variant, and variantAsync. It also adds a bench script, benchmark documentation, and new Vitest benchmark files for array, enum, object, picklist, string pipe, variant, and variantAsync scenarios.

Possibly related PRs

  • open-circle/valibot#1437: Touches the same variant and variantAsync discriminator ~run paths with related dispatch logic.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the PR’s performance benchmarks and optimization changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kibertoad kibertoad changed the title Benchmarks and optimizations feat: Benchmarks and optimizations Jun 30, 2026
@kibertoad kibertoad changed the title feat: Benchmarks and optimizations perf: Benchmarks and optimizations Jun 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@BENCHMARKING.md`:
- Around line 14-16: Update the BENCHMARKING.md description to remove the “runs
every file once” wording, since the benchmark runner executes each case across
warmup and measurement iterations before reporting hz and latency percentiles.
Rewrite the sentence around the benchmark behavior so it accurately describes
repeated iterations, and keep the surrounding guidance in the benchmarking
section consistent with the semantics shown by the bench runner.

In `@library/src/schemas/picklist/picklist.ts`:
- Around line 104-111: The cached _optionsSet in PicklistSchema is holding a
stale snapshot of mutable options, so later mutations to this.options are not
reflected during validation. Update the logic around the optionsSet creation/use
in PicklistSchema (and its parse path) so membership checks always reflect the
current options array, either by rebuilding the Set from this.options each time
or by invalidating the cache whenever options changes. Keep the SameValueZero
behavior consistent with the existing includes-based check.

In `@library/src/schemas/variant/variant.ts`:
- Around line 198-201: The fast dispatch in the variant discriminator lookup is
collapsing an absent key and a present undefined value because it reads
input[this.key] directly. Update the discriminator handling in variant.ts so the
fast path first checks whether the key exists on input the same way the slow
path does (using currentKey in input or equivalent) before consulting
discriminatorMap in the relevant branch, and only use the fast map lookup when
the discriminator is actually present.
- Line 70: The NaN guard in variant validation uses self-comparison, which
triggers Biome. Update the check in the variant schema logic (the `variant.ts`
code path that validates `value`) to use `Number.isNaN(value)` instead of `value
!== value`, preserving the same behavior while keeping lint clean.

In `@library/src/schemas/variant/variantAsync.ts`:
- Line 71: The NaN check in the variant async schema currently uses
self-comparison in the code path around the value guard, which Biome flags as a
lint issue. Update the check in the relevant validation logic within the
variantAsync handling to use Number.isNaN instead, keeping the same behavior
while satisfying the linter.
- Around line 199-203: The async discriminator fast path in variantAsync should
match the sync/slow-path behavior by checking that the discriminator key
actually exists on input before using input[this.key]. Update the
discriminatorMap lookup branch in the variantAsync execution logic so it only
runs when the key is present (for example via a currentKey in input-style
guard), and keep the existing discriminatorMap/option flow otherwise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dad508fc-247c-4a71-a241-5a51a71119f0

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc26ea and 05f65a0.

📒 Files selected for processing (18)
  • BENCHMARKING.md
  • CONTRIBUTING.md
  • library/bench/array.bench.ts
  • library/bench/enum.bench.ts
  • library/bench/object.bench.ts
  • library/bench/picklist.bench.ts
  • library/bench/string-pipe.bench.ts
  • library/bench/variant-async.bench.ts
  • library/bench/variant.bench.ts
  • library/package.json
  • library/src/schemas/enum/enum.ts
  • library/src/schemas/looseObject/looseObject.ts
  • library/src/schemas/object/object.ts
  • library/src/schemas/objectWithRest/objectWithRest.ts
  • library/src/schemas/picklist/picklist.ts
  • library/src/schemas/strictObject/strictObject.ts
  • library/src/schemas/variant/variant.ts
  • library/src/schemas/variant/variantAsync.ts

Comment thread BENCHMARKING.md Outdated
Comment thread library/src/schemas/picklist/picklist.ts
Comment thread library/src/schemas/variant/variant.ts Outdated
Comment thread library/src/schemas/variant/variant.ts Outdated
Comment thread library/src/schemas/variant/variantAsync.ts Outdated
Comment thread library/src/schemas/variant/variantAsync.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

6 issues found across 18 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread library/bench/string-pipe.bench.ts Outdated
Comment thread library/bench/object.bench.ts Outdated
Comment thread BENCHMARKING.md Outdated
Comment thread BENCHMARKING.md Outdated
Comment thread library/src/schemas/enum/enum.ts
Comment thread BENCHMARKING.md Outdated
@kibertoad

Copy link
Copy Markdown
Author

@fabian-hiller any chance you could take a look at this?

@fabian-hiller fabian-hiller self-assigned this Jul 7, 2026
@fabian-hiller fabian-hiller added this to the v1.5 milestone Jul 7, 2026
@fabian-hiller

Copy link
Copy Markdown
Member

Thank you for this PR. I will try to take a closer look and get this merged before v1.5.

@pkg-pr-new

pkg-pr-new Bot commented Jul 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/valibot@1527

commit: c8755bf

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

Labels

enhancement New feature or request performance Its performance related size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants