-
-
Couldn't load subscription status.
- Fork 535
Description
Describe the bug
Summary
We have some large and deeply nested forms.
Some are f.i. for invoices, which can have up to 500 line items, where a line item itself is a nested structure with up to 100 fields.
When using TanStack Form with large forms, validation performance degrades dramatically due to the reactive store architecture triggering cascading updates across all fields, even when only validating a subset of fields.
Impact
- Form size: ~900 fields (9 line items × ~100 fields each)
- Direct Zod validation: 2.4ms
- form.validateAllFields('change'): 2,500ms (1000× slower)
- User experience: 10+ second UI freeze for certain interactions
Environment
@tanstack/react-form: 1.23.0
React: 18.3.1
Zod: 4.1.5
Typescript: 5.9.2
Root Cause Analysis
After profiling with Chrome DevTools and instrumenting the TanStack Form source code, we identified the bottleneck:
Store Architecture (FormApi.ts lines 1015-1293)
The form maintains derived stores that recompute on every state change:
this.fieldMetaDerived = new Derived({
deps: [this.baseStore],
fn: ({ currDepVals }) => {
const fieldMeta = {}
// ❌ LOOPS THROUGH ALL FIELDS on every update
for (const fieldName of Object.keys(currBaseStore.fieldMetaBase)) {
// ... expensive computations for each field
}
return fieldMeta
}
})
this.store = new Derived({
deps: [this.baseStore, this.fieldMetaDerived],
fn: ({ currDepVals }) => {
// ❌ Aggregates ALL field metadata
const fieldMetaValues = Object.values(currFieldMeta)
const isFieldsValidating = fieldMetaValues.some(field => field.isValidating)
const isFieldsValid = fieldMetaValues.every(field => field.isValid)
// ... many more aggregate computations
}
})
validateAllFields Flow (FormApi.ts lines 1492-1517)
validateAllFields = async (cause: ValidationCause) => {
batch(() => {
// ❌ Marks ALL 900 fields as touched
Object.values(this.fieldInfo).forEach((field) => {
field.instance.setMeta((prev) => ({ ...prev, isTouched: true }))
})
})
// ... validation
}
Batch Cascade (@tanstack/store)
Even within batch(), when the batch completes:
- Updates baseStore with all field metadata changes
- Recomputes fieldMetaDerived (loops through all 900 fields)
- Recomputes store (aggregates 900 field states)
- Notifies all React components subscribed to stores
- Triggers React reconciliation
- Garbage collection from thousands of temporary objects
Performance Breakdown
Direct Chrome DevTools profiling showed:
Component Time
─────────────────────────────────
Store updates ~1,000ms
Derived computations ~800ms
React reconciliation ~500ms
Garbage collection ~200ms
─────────────────────────────────
Total (TanStack Form) ~2,500ms
vs. Direct Zod ~2.4ms
Reproduction
const form = useForm({
defaultValues: {
lineItems: Array.from({ length: 9 }, (_, i) => ({
id: i,
product: { /* ~10 fields */ },
delivery: { /* ~30 fields */ },
settlement: { /* ~20 fields */ },
concrete: { /* ~40 fields nested */ }
}))
},
validators: {
onChange: largeZodSchema // validates entire form
}
});
// ❌ This takes 2.5 seconds with 900 fields
await form.validateAllFields('change');
// ✅ Direct Zod takes 2.4ms
const result = largeZodSchema.safeParse(form.state.values);
Additional Context
- The Zod schema itself is not the bottleneck (validated in 2.4ms)
- The issue scales with O(n²): more fields = exponentially worse performance
- Chrome profiler shows "a large number of related field updates in its internal store that trigger a lot of garbage collections"
- This affects any large form, not just our specific use case
Your minimal, reproducible example
tbd
Steps to reproduce
Trigger onChange validation
Expected behavior
Validation performance should scale linearly with the number of fields being validated, not with the total form size.
Actual Behavior: Validation performance scales with total form size regardless of how many fields are actually being validated.
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
- OS: macOS Sequoia 15.6.1
- Browser: Chrome 140
TanStack Form adapter
react-form
TanStack Form version
v1.23.0
TypeScript version
v5.9.2
Additional context
No response