Skip to content

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

Open
GiacoCorsiglia wants to merge 8 commits intoTanStack:mainfrom
GiacoCorsiglia:faster-make-path-array
Open

feat(form-core): 5-10x faster makePathArray#2152
GiacoCorsiglia wants to merge 8 commits intoTanStack: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

  • Refactor

    • Improved path-parsing implementation for better performance and robustness
  • Tests

    • Added benchmark tests for path parsing and updated unit tests to reflect new parsing behavior
    • Added a skipped-by-default performance test suite for field render/mount/unmount timing
  • Chores

    • Added bench scripts and test config to enable benchmarks
    • Ignoring CPU profile files via .gitignore
    • Added a changeset documenting the performance improvement

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR rewrites makePathArray to a single-pass character-code parser, updates unit tests, adds vitest benchmarks and npm scripts, introduces a skipped field-render perf test with optional V8 profiling, and ignores generated .cpuprofile files.

Changes

Path Parsing Optimization and Benchmarking

Layer / File(s) Summary
Core Algorithm: Character-Code Parser
packages/form-core/src/utils.ts
makePathArray reimplemented as single-pass character-code parser with explicit segment handling, digit-to-number conversion, and malformed-input backward compatibility via phantom boundary logic.
Unit Test Coverage
packages/form-core/tests/utils.spec.ts
Tests expanded for numeric handling (lone "0" becomes 0, huge digit strings remain strings, leading zeros preserved mid-path), array copy semantics, type validation, and malformed path corner cases like a]b splitting to ['a','b'].
Benchmarking Infrastructure
packages/form-core/tests/utils.bench.ts, packages/form-core/vite.config.ts, packages/form-core/package.json
Vitest benchmark file captures prior regex-based makePathArrayOld, defines representative test cases, runs side-by-side bench comparisons; Vite config includes benchmark files; package.json adds test:bench and test:bench:dev scripts.
Field Rendering Performance Test
packages/react-form/tests/field-render-perf.test.tsx
New skipped-by-default performance test measures mount/unmount cost across field counts with timing summaries; optional V8 CPU profiling via Node Inspector writes .cpuprofile artifacts when enabled by environment variables.
Build Configuration
.gitignore, .changeset/*
CPU profile files (*.cpuprofile) added to ignore list; a changeset entry documents a patch release for @tanstack/form-core.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through bytes and split each dot,
I watched the parser leap and plot.
Benchmarks hum a steady beat,
Profiles saved for curious feet,
Fields mount light — the code's complete! 🎩

🚥 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 summarizes the main change: rewriting makePathArray for performance. It is specific, concise, and reflects the core contribution of the PR.
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.
Description check ✅ Passed The pull request description comprehensively covers changes, motivation, performance benchmarks, testing, and checklist items with clear detail about the makePathArray rewrite and its impact.

✏️ 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.

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.

I would expect to delete this file before merging.

Comment on lines +15 to +17
* (No `--` separator: with pnpm 10's `--filter`, args after `--` get dropped
* before vitest sees them, so the filename filter is silently ignored and the
* full suite runs instead.)
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.

You can tell Claude wrote this goofy comment 😅

But you can verify that the benchmark results match the real-world behavior visible here.

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.

This can be dropped if we drop the benchmarks

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.

This can be dropped if we drop the benchmarks

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.

I imagine this will be removed before this PR is merged (should that happen)

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 on lines +4 to +6
// Snapshot of the original implementation for side-by-side comparison.
// Remove this and the paired benches once the new implementation is merged.
const reLineOfOnlyDigits = /^(\d+)$/gm
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 8, 2026

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

makePathArrayOld and the "remove this" comment indicate this file (or at least lines 4–68) should not land on main as-is.

The inline makePathArrayOld exists purely for pre/post comparison and is explicitly marked for removal. If the benchmark file is kept post-merge, makePathArrayOld should be removed and the benchmarks restructured to measure only the live implementation (or dropped entirely if the intent was never to ship them).

🤖 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/tests/utils.bench.ts` around lines 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.

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.

Correct.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@GiacoCorsiglia, thanks for confirming! Would you like me to help clean up the benchmark file by removing makePathArrayOld, the "remove this" comment, and the paired comparison benches — leaving only the benchmarks for the current makePathArray implementation? I can plan those changes if that's helpful.

(ノ◕ヮ◕)ノ*:・゚✧

* 2000 2709.0ms 8351.3ms 11060.6ms 10882.0..11462.1ms
* ==============================================================
*
* NOTE: This file is intended to be removed before merge.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

File is self-marked for removal before merge.

Line 29 reads: "NOTE: This file is intended to be removed before merge." The PR description also calls these files "intended temporary." If this is not meant to land in main, it should be dropped before the PR is merged.

Happy to draft a follow-up issue to track a permanent, CI-safe perf regression test if that would help preserve the intent without the throwaway file.

🤖 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/react-form/tests/field-render-perf.test.tsx` at 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.

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.

I leave it to the maintainers whether they want such a thing!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@GiacoCorsiglia Fair enough! The original concern — that the file is self-marked for removal — still stands for maintainers to decide before merging. If they'd like, I'm happy to draft a follow-up GitHub issue to track adding a permanent, CI-safe perf regression test. Just let me know!

Comment thread packages/react-form/tests/field-render-perf.test.tsx
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.

1 participant