Skip to content

Benchmarks

Last updated 2026-07-27 — neutro/form v0.5.1

Environment

Runnerlocal (darwin)
Nodev20.19.0
BrowserChromium (Playwright)

Competitor Versions

LibraryVersion
react-hook-formv7.80.0
formikv2.4.9
vee-validatev4.15.1
feltev1.3.0
tanstack-form (React)v1.33.0
tanstack-form (Svelte)v1.33.0

Results reflect these exact releases — a later competitor update may change outcomes, so check the version above before drawing conclusions.

Methodology

Three dimensions: correctness (PASS/FAIL), browser performance (Playwright Chromium), and bundle size (esbuild + gzip). Badges are always relative to neutro/form:

  • Win — neutro beats this library by more than 10%
  • Tied — within 10% either way
  • Behind — neutro trails by more than 10%, no documented reason
  • ⚖️ Tradeoff — neutro trails for a documented design reason, or neutro passes a check this library architecturally can't (a harsh "neutro wins" is softened to Tradeoff instead) — see footnotes
  • N/A — surface doesn't apply to this library

Scorecard

Correctness

Libraryarray-state-integrityasync-racedependency-trigger
felte— N/A— N/A— N/A
formik— N/A [1]— N/A [2]— N/A [3]
react-hook-form— N/A [4]— N/A [5]— N/A [6]
tanstack-form— N/A [7]— N/A [8]— N/A [9]
tanstack-form (React)— N/A— N/A— N/A
tanstack-form (Svelte)— N/A— N/A— N/A
vee-validate— N/A [10]— N/A [11]— N/A [12]

Performance

Libraryre-renders/10re-renders/100async-latencyarray-opsasync-cancellation
felte✅ Win [13]✅ Win [14]⚖️ Tradeoff [15]✅ Win [16]➖ Tied
formik✅ Win [17]✅ Win [18]⚖️ Tradeoff [19]✅ Win [20]⚖️ Tradeoff [21]
react-hook-form➖ Tied [22]➖ Tied [23]⚖️ Tradeoff [24]➖ Tied [25]➖ Tied
tanstack-form— N/A— N/A— N/A— N/A— N/A
tanstack-form (React)➖ Tied [26]➖ Tied [27]⚖️ Tradeoff [28]✅ Win [29]➖ Tied
tanstack-form (Svelte)➖ Tied [30]➖ Tied [31]⚖️ Tradeoff [32]⚖️ Tradeoff [33]➖ Tied
vee-validate➖ Tied [34]➖ Tied [35]⚖️ Tradeoff [36]➖ Tied [37]➖ Tied

Size

Librarybundle-size
felte✅ Win [38]
formik✅ Win [39]
react-hook-form❌ Behind [40]
tanstack-form✅ Win [41]
tanstack-form (React)— N/A
tanstack-form (Svelte)— N/A
vee-validate❌ Behind [42]

Correctness

array-state-integrity

LibraryResultWhy
neutro/form✅ PASSerrors/touched/dirty state is rekeyed by index on every array splice/move/swap
tanstack-form— N/Ano public API to rekey per-field error/touched state on array splice outside React context
react-hook-form— N/Astate-map rekey on splice not exposed outside hook context
formik— N/Astate-map rekey on splice not exposed outside hook context
vee-validate— N/Astate-map rekey on splice not exposed outside hook context

async-race

LibraryResultWhy
neutro/form✅ PASSeach async validation run gets its own AbortController; stale results are discarded by epoch
tanstack-form— N/Ano async cancellation API in vanilla usage
react-hook-form— N/Ano async cancellation API in vanilla usage
formik— N/Ano async cancellation API in vanilla usage
vee-validate— N/Ano async cancellation API in vanilla usage

dependency-trigger

LibraryResultWhy
neutro/form✅ PASSa static dependency graph is precompiled at form init, so dependent fields re-validate automatically
tanstack-form— N/Arequires per-field validators; no declarative cross-field dependency graph
react-hook-form— N/Ano declarative dependency graph; cross-field validation is manual
formik— N/Ano declarative dependency graph; cross-field validation is manual
vee-validate— N/Ano declarative dependency graph; cross-field validation is manual

validation-scope-precision

LibraryResultWhy
neutro/form✅ PASSset() on a field with 3 declared dependents validates exactly itself + those 3 (4 of 504 total fields), not the whole form — the O(1) precomputed dependency-graph claim, quantified

Browser (Chromium / Playwright, production build, no StrictMode)

Dependency Chain — Settle Latency (200-field validation cascade)

LibrarySettle latency
neutro/form (React)87.9ms[43]
react-hook-form24.5ms
tanstack-form (React)156.0ms
neutro/form (Vue)23.2ms[44]
vee-validate23.4ms
neutro/form (Svelte)24.4ms[45]
tanstack-form (Svelte)167.6ms
formik— N/A[46]
felte— N/A[47]

Array Operations (remove + move, render count)

Note: render counts are not directly comparable across all libraries on this surface — some libraries (e.g. TanStack Form) isolate counters per array index, while others (e.g. neutro/form, Felte) increment counters for every item in the array on any mutation. A low count does not necessarily indicate less DOM work.

LibraryRenders
neutro/form (React)18
react-hook-form18
formik54
tanstack-form (React)24
neutro/form (Vue)18
vee-validate18
neutro/form (Svelte)21
tanstack-form (Svelte)0[33:1]
felte47

DOM Cleanup (connect/disconnect, neutro only)

LibraryConnected after cleanup
neutro/form (React)0
neutro/form (Vue)0
neutro/form (Svelte)0

Memory Churn (heap delta across mount/unmount cycles, post-GC)

LibraryHeap delta (post-GC)
neutro/form (React)590.7 KB
react-hook-form635.9 KB

Async Cancellation (stale-result race)

LibraryCancellation
neutro/form (React)
react-hook-form
formik[21:1]
tanstack-form (React)
neutro/form (Vue)
vee-validate
neutro/form (Svelte)
tanstack-form (Svelte)
felte

Re-renders per 20-keystroke sequence (10-field form)

LibraryRenders
neutro/form (React)20
react-hook-form20
formik400
tanstack-form (React)20
neutro/form (Vue)20
vee-validate20
neutro/form (Svelte)20
tanstack-form (Svelte)20
felte200

Mount Cost (time to interactive, Navigation Timing API)

LibraryTime to interactive
neutro/form (React)3.5ms
react-hook-form5.3ms
formik4.5ms
tanstack-form (React)5.8ms
neutro/form (Vue)3.4ms
vee-validate5.5ms
neutro/form (Svelte)3.9ms
tanstack-form (Svelte)4.1ms
felte4.0ms

Schema Validation — Submit Latency (Zod, click-to-error-visible)

LibrarySubmit latency
neutro/form (React)39.3ms
react-hook-form44.9ms
tanstack-form (React)66.1ms
neutro/form (Vue)55.4ms
vee-validate75.7ms
neutro/form (Svelte)48.7ms
tanstack-form (Svelte)49.8ms
felte49.2ms
formik— N/A[48]

Schema Validation — Re-renders per 20-keystroke sequence (Zod, 10-field form)

LibraryRenders
neutro/form (React)20
react-hook-form0
tanstack-form (React)200
neutro/form (Vue)20
vee-validate20
neutro/form (Svelte)20
tanstack-form (Svelte)200
felte200
formik— N/A[49]

Re-renders per 20-keystroke sequence (100-field form)

LibraryRenders
neutro/form (React)20
react-hook-form20
formik4000
tanstack-form (React)20
neutro/form (Vue)20
vee-validate20
neutro/form (Svelte)20
tanstack-form (Svelte)20
felte2000

Async Validation Latency

Libraryp50p99
neutro/form (React)302ms[50]303ms
react-hook-form202ms203ms
formik203ms203ms
tanstack-form (React)201ms202ms
neutro/form (Vue)302ms[51]304ms
vee-validate203ms203ms
neutro/form (Svelte)303ms[52]303ms
tanstack-form (Svelte)201ms202ms
felte202ms203ms

Async Validation Latency — Debounce Floor (neutro only)

Libraryp50p99
neutro/form (React) [debounce=0]203ms203ms
neutro/form (Vue) [debounce=0]202ms203ms
neutro/form (Svelte) [debounce=0]202ms203ms

Bundle Size

LibraryGzip size
neutro/form11.9 KB
neutro/form (minimal)8.0 KB
react-hook-form9.7 KB
formik13.2 KB
tanstack-form17.4 KB
vee-validate10.4 KB
felte22.9 KB

Architecture Notes

DOM cleanup (dom-cleanup row above, neutro only): neutro/form's connect/disconnect lifecycle registers a WeakRef per connected field in an internal registry, pruned by a MutationObserver watching for node removal. The "Connected after cleanup" number confirms this registry returns to 0 after mount/unmount churn — competitor libraries have no equivalent connect/disconnect API to compare against, so this section has no comparison table.



  1. formik — state-map rekey on splice not exposed outside hook context ↩︎

  2. formik — no async cancellation API in vanilla usage ↩︎

  3. formik — no declarative dependency graph; cross-field validation is manual ↩︎

  4. react-hook-form — state-map rekey on splice not exposed outside hook context ↩︎

  5. react-hook-form — no async cancellation API in vanilla usage ↩︎

  6. react-hook-form — no declarative dependency graph; cross-field validation is manual ↩︎

  7. tanstack-form — no public API to rekey per-field error/touched state on array splice outside React context ↩︎

  8. tanstack-form — no async cancellation API in vanilla usage ↩︎

  9. tanstack-form — requires per-field validators; no declarative cross-field dependency graph ↩︎

  10. vee-validate — state-map rekey on splice not exposed outside hook context ↩︎

  11. vee-validate — no async cancellation API in vanilla usage ↩︎

  12. vee-validate — no declarative dependency graph; cross-field validation is manual ↩︎

  13. felte — neutro/form: 20 renders vs felte: 200 renders (900% fewer/faster) ↩︎

  14. felte — neutro/form: 20 renders vs felte: 2000 renders (9900% fewer/faster) ↩︎

  15. felte — same debounce policy as React — see debounce=0 column. ↩︎

  16. felte — neutro/form: 21 renders vs felte: 47 renders (124% fewer/faster) ↩︎

  17. formik — neutro/form: 20 renders vs formik: 400 renders (1900% fewer/faster) ↩︎

  18. formik — neutro/form: 20 renders vs formik: 4000 renders (19900% fewer/faster) ↩︎

  19. formik — neutro debounces async validation 300ms by default (asyncDebounceMs) to avoid firing on every keystroke. See the debounce=0 column for the floor cost. ↩︎

  20. formik — neutro/form: 18 renders vs formik: 54 renders (200% fewer/faster) ↩︎

  21. formik — no async cancellation API ↩︎ ↩︎

  22. react-hook-form — neutro/form: 20 renders vs react-hook-form: 20 renders ↩︎

  23. react-hook-form — neutro/form: 20 renders vs react-hook-form: 20 renders ↩︎

  24. react-hook-form — neutro debounces async validation 300ms by default (asyncDebounceMs) to avoid firing on every keystroke. See the debounce=0 column for the floor cost. ↩︎

  25. react-hook-form — neutro/form: 18 renders vs react-hook-form: 18 renders ↩︎

  26. tanstack-form (React) — neutro/form: 20 renders vs tanstack-form (React): 20 renders ↩︎

  27. tanstack-form (React) — neutro/form: 20 renders vs tanstack-form (React): 20 renders ↩︎

  28. tanstack-form (React) — neutro debounces async validation 300ms by default (asyncDebounceMs) to avoid firing on every keystroke. See the debounce=0 column for the floor cost. ↩︎

  29. tanstack-form (React) — neutro/form: 18 renders vs tanstack-form (React): 24 renders (33% fewer/faster) ↩︎

  30. tanstack-form (Svelte) — neutro/form: 20 renders vs tanstack-form (Svelte): 20 renders ↩︎

  31. tanstack-form (Svelte) — neutro/form: 20 renders vs tanstack-form (Svelte): 20 renders ↩︎

  32. tanstack-form (Svelte) — same debounce policy as React — see debounce=0 column. ↩︎

  33. tanstack-form (Svelte) — TanStack's own Svelte bench harness never defines window.__resetArrayRenders, so its render counter (window.__tanstackArrayRenders) stays permanently empty and reports an artificial 0 — not a real absence of render work. Confirmed by direct inspection during this project's own v0.5.0 verification; not a neutro/form architectural gap. ↩︎ ↩︎

  34. vee-validate — neutro/form: 20 renders vs vee-validate: 20 renders ↩︎

  35. vee-validate — neutro/form: 20 renders vs vee-validate: 20 renders ↩︎

  36. vee-validate — same debounce policy as React — see debounce=0 column. ↩︎

  37. vee-validate — neutro/form: 18 renders vs vee-validate: 18 renders ↩︎

  38. felte — neutro/form: 11.9 KB vs felte: 22.9 KB (93% fewer/faster) ↩︎

  39. formik — neutro/form: 11.9 KB vs formik: 13.2 KB (11% fewer/faster) ↩︎

  40. react-hook-form — neutro/form: 11.9 KB vs react-hook-form: 9.7 KB ↩︎

  41. tanstack-form — neutro/form: 11.9 KB vs tanstack-form: 17.4 KB (46% fewer/faster) ↩︎

  42. vee-validate — neutro/form: 11.9 KB vs vee-validate: 10.4 KB ↩︎

  43. neutro/form (React) — This surface’s settle-latency measurement is bounded below by Playwright’s waitForFunction polling granularity, not by real cascade cost — for very fast libraries the reported number reflects polling overhead more than actual work. Treat the gap between neutro and the manual-wiring competitors as a floor on the true gap, not an exact figure. ↩︎

  44. neutro/form (Vue) — This surface’s settle-latency measurement is bounded below by Playwright’s waitForFunction polling granularity, not by real cascade cost — for very fast libraries the reported number reflects polling overhead more than actual work. Treat the gap between neutro and the manual-wiring competitors as a floor on the true gap, not an exact figure. ↩︎

  45. neutro/form (Svelte) — This surface’s settle-latency measurement is bounded below by Playwright’s waitForFunction polling granularity, not by real cascade cost — for very fast libraries the reported number reflects polling overhead more than actual work. Treat the gap between neutro and the manual-wiring competitors as a floor on the true gap, not an exact figure. ↩︎

  46. formik — Formik has no live field-to-field subscription API to drive a manual validation cascade of this shape — excluded from this comparison. Formik participates fully in every other browser surface. ↩︎

  47. felte — Felte has no field-level watch API to drive a manual validation cascade of this shape — excluded from this comparison. Felte participates fully in every other browser surface. ↩︎

  48. formik — Formik has no first-party Zod integration (only an unofficial zod-formik-adapter package) — excluded from this Zod-specific comparison. Formik participates fully in every other browser surface. ↩︎

  49. formik — Formik has no first-party Zod integration (only an unofficial zod-formik-adapter package) — excluded from this Zod-specific comparison. Formik participates fully in every other browser surface. ↩︎

  50. neutro/form (React) — neutro debounces async validation 300ms by default (asyncDebounceMs) to avoid firing on every keystroke. See the debounce=0 column for the floor cost. ↩︎

  51. neutro/form (Vue) — same debounce policy as React — see debounce=0 column. ↩︎

  52. neutro/form (Svelte) — same debounce policy as React — see debounce=0 column. ↩︎