Skip to content

feat(form-core): 5-10x faster makePathArray#2152

Merged
crutchcorn merged 11 commits into
TanStack:mainfrom
GiacoCorsiglia:faster-make-path-array
May 26, 2026
Merged

feat(form-core): 5-10x faster makePathArray#2152
crutchcorn merged 11 commits into
TanStack:mainfrom
GiacoCorsiglia:faster-make-path-array

Conversation

@GiacoCorsiglia
Copy link
Copy Markdown

@GiacoCorsiglia GiacoCorsiglia commented May 8, 2026

🎯 Changes

NB: I used Claude Code when doing this work (but not to write this description).

In #2150 I noted that mounting <form.Field> is slow because it exhibits O(N^2) complexity, where N is the number of fields in the form. This PR does not fix the O(N^2) behavior, but when profiling it, I noticed that makePathArray is a super hot path.

On main, makePathArray includes a bunch of regex string munging. I rewrote it as a single-pass for loop. This produces 5–10x speed up in microbenchmarks, and what appears to be ~2x faster <form.Field> mounting/unmounting.

I also wrote additional tests for makePathArray to try to capture its edge case behavior (what I think are malformed inputs—things not allowed by DeepKeys<T>). I did my best to maintain backwards compatibility, but you will see there is one BC-break I identified:

// Old behavior:
expect(makePathArray('a]b')).toEqual(['ab'])
// New behavior:
expect(makePathArray('a]b')).toEqual(['a', 'b'])

I may have missed some edge cases not covered by my new tests. It's possible to match the old behavior more closely. Claude's original implementation was much more complex and handled more edge cases, but I opted to simplify so that the code was easier to understand. Happy to adjust.

Benchmarks

This branch includes benchmark files I would not expect to be merged, but wanted to include temporarily so others can validate my results (run on my M4 Pro MackBook Pro).

Here's the output for the new utils.bench.ts:

 BENCH  Summary

   @tanstack/form-core  old - tests/utils.bench.ts > array input (fast path, no parsing)
    1.10x faster than new

   @tanstack/form-core  new - tests/utils.bench.ts > simple key (no nesting)
    7.56x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > uuid key
    4.78x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > dot notation
    5.90x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > mixed dot and bracket notation
    11.81x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > deeply nested mixed path
    11.61x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > numeric string with leading zeros (kept as string)
    9.95x faster than old

   @tanstack/form-core  new - tests/utils.bench.ts > numeric string (converted to number)
    9.67x faster than old

In addition, I (well, Claude) wrote field-render-perf.test.tsx, which is an automated version of the reproduction I put together for #2150.

Results with the old makePathArray():

iterations=5 warmup=1 env=jsdom (median of 5)
    N      mount    unmount      total        total min..max
  100     17.9ms     18.5ms     36.3ms          35.3..45.8ms
  500    185.1ms    419.9ms    601.5ms        599.8..612.1ms
 1000    691.2ms   1897.7ms   2549.5ms      2457.9..2677.3ms
 2000   2709.0ms   8351.3ms  11060.6ms    10882.0..11462.1ms

Results with the new makePathArray():

iterations=5 warmup=1 env=jsdom (median of 5)
    N      mount    unmount      total        total min..max
  100     13.2ms      7.6ms     20.9ms          18.1..26.8ms
  500     86.0ms    130.4ms    218.2ms        213.2..232.6ms
 1000    293.7ms    719.7ms   1015.8ms       929.1..1081.8ms
 2000   1190.4ms   3499.1ms   4705.3ms      4456.6..4951.0ms

~2x faster!

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Performance
    • Improved form field mounting and unmounting performance.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0aab4eaa-29fd-4713-85bb-4ea3198a7387

📥 Commits

Reviewing files that changed from the base of the PR and between 475b5c7 and 5ca4c18.

📒 Files selected for processing (2)
  • packages/form-core/src/utils.ts
  • packages/form-core/tests/utils.spec.ts

📝 Walkthrough

Walkthrough

The PR optimizes the path parsing function makePathArray by replacing a regex-based transformation pipeline with a single-pass character-code parser. The implementation includes numeric segment conversion with precision safety, digit-only validation, and malformed-input compatibility. Comprehensive unit tests verify the new behavior across numeric handling, edge cases, and invalid inputs.

Changes

Path Parsing Optimization

Layer / File(s) Summary
Core Parser Implementation
packages/form-core/src/utils.ts
Character-code constants (DOT_CHAR_CODE, LBRACKET_CHAR_CODE, RBRACKET_CHAR_CODE, etc.) and a rewritten makePathArray function that uses single-pass character iteration to split segments on separators, convert digit-only segments to numbers with leading-zero preservation and precision safeguards, handle optional leading brackets, and maintain backward-compatible malformed-path behavior via phantom-boundary flushing.
Unit Test Coverage
packages/form-core/tests/utils.spec.ts
Removed assertion for simple digit-string conversion; added test cases for digit strings exceeding safe integer bounds (remain strings), leading-zero preservation in mid-path dot and bracket notation, lone string "0" converts to numeric 0, defensive array copying, type validation (throw on non-string/non-array), and expanded malformed-path tokenization expectations including a]b['a', 'b'].
Release Configuration
.gitignore, .changeset/soft-views-tease.md
Added .cpuprofile glob to .gitignore to exclude generated profiling artifacts; created changeset entry declaring a patch release for @tanstack/form-core with note about field mount/unmount performance improvements.

🎯 3 (Moderate) | ⏱️ ~25 minutes


🐰 A path once parsed with regex and care,
Now flies through chars, from first to last affair,
Each digit tallied, each bracket aligned,
Performance blooms when algorithms shine—
Tests verify the journey, safe and true,
Hop on, dear form, performance waits for you!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: rewriting makePathArray for significant performance improvement (5-10x faster).
Description check ✅ Passed The description covers all required template sections: detailed explanation of changes with context, completed checklist items, and confirmed changeset generation for published code.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Comment thread packages/react-form/tests/field-render-perf.test.tsx Outdated
Comment thread packages/react-form/tests/field-render-perf.test.tsx Outdated
Comment thread packages/form-core/vite.config.ts
Comment thread packages/form-core/package.json
Comment thread packages/form-core/tests/utils.bench.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/form-core/src/utils.ts (1)

227-230: 💤 Low value

Incomplete comment — "for these because." is a sentence fragment.

✏️ Suggested fix
-  // for these because.
+  // for these, because the old regex pipeline always produced at least one empty string
+  // from the split even when the input was only separator/meta characters.
🤖 Prompt for 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.

In `@packages/form-core/src/utils.ts` around lines 227 - 230, The comment above
the conditional that checks "if (!result.length) result.push('')" is a fragment;
update it to a complete sentence that explains why the old implementation
returned [''] (i.e., when the input contained only phantom characters like ']',
'[]', '[]]' producing no segments, the old behavior produced a single empty
segment to represent an empty path). Edit the comment near the result variable
and the conditional so it reads as a full explanatory sentence referencing the
phantom-char inputs and the intended empty-segment fallback.
🤖 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 `@packages/form-core/src/utils.ts`:
- Around line 188-198: The current logic in the segment parsing branch
(variables: treatAsNumber, allDigits, segLen, str.charCodeAt(segStart) ===
CC_ZERO, parseInt call and result.push) unconditionally pushes parseInt(...)
which loses precision for integers > Number.MAX_SAFE_INTEGER; add a round-trip
guard: when treatAsNumber is true, parse the segment into a number (e.g., parsed
= parseInt(str.slice(segStart, i), 10)) but only push parsed if String(parsed)
=== the original segment string; otherwise push the original slice
(str.slice(segStart, i)) so large integer strings are preserved as strings.

In `@packages/form-core/tests/utils.bench.ts`:
- Around line 4-6: The file contains a temporary snapshot and an inline legacy
function makePathArrayOld plus a "remove this" comment and paired benches that
must not land on main; remove the makePathArrayOld function and the accompanying
comment and any benchmark comparisons that reference it (e.g., paired benches)
and restructure the test to benchmark only the current live implementation (or
delete the whole bench file if benchmarking was never intended to be shipped);
ensure any helper regex like reLineOfOnlyDigits and tests only used by the old
implementation are also removed or repurposed so the file contains solely the
intended, production benchmark code.

In `@packages/react-form/tests/field-render-perf.test.tsx`:
- Line 29: This test file is marked for removal ("NOTE: This file is intended to
be removed before merge.") and should not land in main; remove the temporary
perf test from the PR (or move it out of the commit into a dedicated draft
branch) and instead open a follow-up issue or add a CI-safe permanent test in
the proper tests directory so the throwaway file does not get merged.
- Around line 178-184: The test uses import.meta.dirname when building mountPath
and unmountPath which fails on Node <21.2.0; add a fallback that computes a
dirname from fileURLToPath(import.meta.url) and use that variable instead of
import.meta.dirname. Specifically, near the top of the test module define a
const (e.g., testDir or dirname) that sets dirname = import.meta.dirname ??
path.dirname(fileURLToPath(import.meta.url)) and then update the join calls that
create mountPath and unmountPath (and any other uses of import.meta.dirname) to
use that dirname; reference functions/symbols: mountPath, unmountPath,
PROFILE_N, join, import.meta.dirname, import.meta.url, fileURLToPath.

---

Nitpick comments:
In `@packages/form-core/src/utils.ts`:
- Around line 227-230: The comment above the conditional that checks "if
(!result.length) result.push('')" is a fragment; update it to a complete
sentence that explains why the old implementation returned [''] (i.e., when the
input contained only phantom characters like ']', '[]', '[]]' producing no
segments, the old behavior produced a single empty segment to represent an empty
path). Edit the comment near the result variable and the conditional so it reads
as a full explanatory sentence referencing the phantom-char inputs and the
intended empty-segment fallback.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2789ad5-a68f-4f50-a809-80d8c5d11698

📥 Commits

Reviewing files that changed from the base of the PR and between cab571a and 015ea3a.

📒 Files selected for processing (7)
  • .gitignore
  • packages/form-core/package.json
  • packages/form-core/src/utils.ts
  • packages/form-core/tests/utils.bench.ts
  • packages/form-core/tests/utils.spec.ts
  • packages/form-core/vite.config.ts
  • packages/react-form/tests/field-render-perf.test.tsx

Comment thread packages/form-core/src/utils.ts
Comment thread packages/form-core/tests/utils.bench.ts Outdated
Comment thread packages/react-form/tests/field-render-perf.test.tsx Outdated
Comment thread packages/react-form/tests/field-render-perf.test.tsx Outdated
@crutchcorn
Copy link
Copy Markdown
Member

This is awesome, thank you!

Consider the new code "LGTM"'d. Let's go ahead and remove the old code, remove benchmarking, and generally clean up the PR to land in main

Let me know what you need from us!

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 10, 2026

View your CI Pipeline Execution ↗ for commit 5ca4c18

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 1m 30s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 36s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-26 14:26:37 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 10, 2026

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@2152

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@2152

@tanstack/form-devtools

npm i https://pkg.pr.new/@tanstack/form-devtools@2152

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@2152

@tanstack/preact-form

npm i https://pkg.pr.new/@tanstack/preact-form@2152

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@2152

@tanstack/react-form-devtools

npm i https://pkg.pr.new/@tanstack/react-form-devtools@2152

@tanstack/react-form-nextjs

npm i https://pkg.pr.new/@tanstack/react-form-nextjs@2152

@tanstack/react-form-remix

npm i https://pkg.pr.new/@tanstack/react-form-remix@2152

@tanstack/react-form-start

npm i https://pkg.pr.new/@tanstack/react-form-start@2152

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@2152

@tanstack/solid-form-devtools

npm i https://pkg.pr.new/@tanstack/solid-form-devtools@2152

@tanstack/svelte-form

npm i https://pkg.pr.new/@tanstack/svelte-form@2152

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@2152

commit: 5ca4c18

@sentry
Copy link
Copy Markdown

sentry Bot commented May 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.46%. Comparing base (6892ed0) to head (475b5c7).
⚠️ Report is 200 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2152      +/-   ##
==========================================
+ Coverage   90.35%   90.46%   +0.10%     
==========================================
  Files          38       49      +11     
  Lines        1752     2065     +313     
  Branches      444      546     +102     
==========================================
+ Hits         1583     1868     +285     
- Misses        149      177      +28     
  Partials       20       20              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread .gitignore
stats-hydration.json
stats.json
stats.html
*.cpuprofile
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Figured I might as well leave this

@GiacoCorsiglia
Copy link
Copy Markdown
Author

@crutchcorn Amazing! I removed all the benchmarks and cleaned up the comments in the new tests, so I think it's ready to be merged now. But let me know if you need anything else.

@RobertBeekman
Copy link
Copy Markdown

Just a little bump as it has been 2 weeks. Would be great to see this merged, we've run into the same performance bottleneck :)

Copy link
Copy Markdown
Member

@crutchcorn crutchcorn left a comment

Choose a reason for hiding this comment

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

Sorry for delay in merging - been overwhelmed lately with other stuff. Once it passes CI, we'll merge and release

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.43%. Comparing base (6892ed0) to head (5ca4c18).
⚠️ Report is 221 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2152      +/-   ##
==========================================
+ Coverage   90.35%   91.43%   +1.08%     
==========================================
  Files          38       59      +21     
  Lines        1752     2336     +584     
  Branches      444      582     +138     
==========================================
+ Hits         1583     2136     +553     
- Misses        149      179      +30     
- Partials       20       21       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@crutchcorn crutchcorn merged commit d0d941d into TanStack:main May 26, 2026
10 checks passed
@github-actions github-actions Bot mentioned this pull request May 26, 2026
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.

4 participants