Skip to content

Performance degradation with large forms: validateAllFields triggers O(n²) store updates #1786

@drdla

Description

@drdla

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:

  1. Updates baseStore with all field metadata changes
  2. Recomputes fieldMetaDerived (loops through all 900 fields)
  3. Recomputes store (aggregates 900 field states)
  4. Notifies all React components subscribed to stores
  5. Triggers React reconciliation
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions