Benchmarks

How Attaform holds up across the Vue form-library field on the same demanding forms, measured in a real browser. Bundle size, supply-chain health, and per-scenario runtime, with every number traced to the run that produced it.

This page is an honest scoreboard. The numbers come from apps/bench-arena, a self-contained harness that drives every library through identical scenarios in real Chromium via Playwright, then writes one provenance-stamped results.json that this page renders directly. Nothing is hand-entered. Where Attaform leads, the run says so; where it pays a cost, the run says that too. Both ship from the same green run.

How to read this

A fair cross-library comparison has to account for the fact that these libraries do different amounts of work. The harness handles that with a few rules:

  • Layers are the fairness axis. Form-state libraries (Attaform among them) own reactive values, validation, and input binding. Validation-only libraries own validation against state you wire yourself. A batteries-included library renders its own inputs. Each row is labeled with its layer, so a validation-only engine mounting faster than a full form-state library is read as owning less, not as winning.
  • The DOM is held constant. Every headless library drives the same bare <input> markup over the same field count and the same schema, so a runtime number reflects the library's own machinery, not its choice of components.
  • Every library runs in its fastest idiomatic configuration. Debounces are neutralized, validation triggers are normalized, and array and union primitives use each library's native fast path. Attaform is measured on its shipping default, strict mode, never a relaxed setting.
  • Real builds, pinned validators. Attaform is consumed as its published dist, the same artifact an installer gets, minified in the same build as every other library. Zod v3 is pinned across the Zod-capable cohort.
  • Numbers normalize two ways. A ratio compares each library to Attaform at the same size. A slope compares a library to itself at the scenario's smallest size, so the shape of growth survives a change of machine.

The harness, every adapter, and the scenario generators live in apps/bench-arena. Found a fairer configuration for a library? The adapters are small and the README invites a pull request.

What it costs to adopt

Before any runtime number, three figures decide whether a library is worth reaching for: what it can express, what it adds to your bundle, and how its supply chain scores.

Capability coverage

What each library expresses as a first-class primitive versus composes by hand. A gap here becomes honest expressiveness data, never a rigged timing loss: a shape a library cannot express idiomatically is left out of its runtime rows rather than forced into a slow number.

LibraryLayerFlatDeeply nestedDynamic arraysGridDiscriminated unionMassiveWizard
Attaform (Zod 3) Zod 3Form stateNativeNativeNativeNativeNativeNativeNative
Attaform (Zod 4) Zod 4Form stateNativeNativeNativeNativeNativeNativeNative
vee-validate Zod 3Form stateNativeNativeNativeNativeHand-rolledNativeHand-rolled
@tanstack/vue-form Zod 3Form stateNativeNativeNativeNativeHand-rolledNativeHand-rolled
@formisch/vue ValibotForm stateNativeNativeNativeNativeHand-rolledNativeHand-rolled
Regle (schema) Zod 3Validation onlyNativeNativeNativeNativeNativeNativeHand-rolled
Regle (rules) native rulesValidation onlyNativeNativeNativeNativeHand-rolledNativeHand-rolled
FormKit Zod 3Batteries includedNativeNativeNativeNativeHand-rolledNativeHand-rolled
Vuelidate native rulesValidation onlyNativeNativeHand-rolledHand-rolledHand-rolledNativeHand-rolled

Native: a first-class primitive. Hand-rolled: composed from lower-level pieces. Dash: not expressed, which the runtime tables read as no number, never a slow one.

Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Bundle size

Attaform is the heaviest in the cohort. That is the cost of shipping reactive form state, schema validation binding, persistence, undo and redo, and a multi-step wizard in one zero-dependency package, and it is the honest price of admission. The figure is the full bundle; an app that route-splits its forms pulls less on a first paint.

LibraryGzippedvs AttaformValidator
@formisch/vue
3.6 kB
0.06×valibot 1.4.1
Vuelidate
5.0 kB
0.08×native validators
vee-validate
24.6 kB
0.41×zod 3.25.76
@tanstack/vue-form
30.0 kB
0.5×zod 3.25.76
Regle
31.5 kB
0.53×zod 3.25.76
FormKit
43.2 kB
0.72×zod 3.25.76
Attaform
59.9 kB
zod 3.25.76

Each row is the same minimal real form (one text field, one email field, schema-validated, a submit handler) in that library's idiomatic API, with its validator weighed in. Vue is external, since every app ships it once. Attaform's figure is its full bundle; with route-level code-splitting a first paint pulls less.

Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Supply-chain health

The OpenSSF Scorecard rates a project's adoption of supply-chain practices: branch protection, signed releases, pinned dependencies, CI hardening, and more. For a form library headed into an audited setting, that posture is part of the cost of adoption, so the benchmark stamps each project's current score alongside the size and runtime figures.

LibraryOpenSSF ScorecardAs ofLink
Attaform (Zod 3)7.7 / 102026-06-15 Scorecard
Attaform (Zod 4)7.7 / 102026-06-15 Scorecard
vee-validate3.6 / 102026-06-08 Scorecard
@tanstack/vue-formNot published Repository
@formisch/vueNot published Repository
Regle (schema)Not published Repository
Regle (rules)Not published Repository
FormKitNot published Repository
Vuelidate4.4 / 102026-06-08 Scorecard

The OpenSSF Scorecard rates a project's adoption of supply-chain practices out of 10. An absent score has two meanings, kept distinct here. Not published means the project has not opted into a Scorecard, which is a choice, not a deficiency. Unavailable means the lookup did not complete on this run, a network gap on our side and never a statement about the project. Scores are point-in-time; the linked viewer shows the live result.

Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Typing into a form

Keystroke latency

The headline interaction. A keystroke runs the value write, validation, and the re-render it triggers. On a flat form Attaform clears a 60 fps frame budget with room to spare.

Flat: Keystroke latency

In the leading group of form-state libraries on this run.

LibraryF10F50
@formisch/vue
0.50 ms
0.70 ms 0.87×
vee-validate
0.50 ms
0.70 ms 0.88×
Vuelidate
0.40 ms 0.8×
0.70 ms 0.88×
Attaform (Zod 3)
0.50 ms
0.80 ms
Attaform (Zod 4)
0.50 ms
0.80 ms
Regle (rules)
0.50 ms
0.80 ms
FormKit
0.70 ms 1.4×
0.90 ms 1.13×
@tanstack/vue-form
0.50 ms
1.00 ms 1.25×
Regle (schema)
0.60 ms 1.2×
1.00 ms 1.25×
Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

At five thousand fields the picture tightens. The harness reports where Attaform lands plainly, and it is a scenario worth a future look.

Massive: Keystroke latency

Near the front of the form-state pack here.

LibraryL2000L5000
Vuelidate
12.4 ms 0.75×
21.2 ms 0.68×
Regle (rules)
13.1 ms 0.79×
21.2 ms 0.68×
vee-validate
13.1 ms 0.79×
21.9 ms 0.7×
@formisch/vue
13.2 ms 0.8×
23.5 ms 0.75×
Attaform (Zod 3)
16.6 ms
31.2 ms
Attaform (Zod 4)
17.0 ms 1.02×
31.3 ms
Regle (schema)
23.5 ms 1.42×
50.9 ms 1.63×
@tanstack/vue-form
29.8 ms 1.8×
81.9 ms 2.62×
FormKit
5.20 ms 0.31×
did not finish> 5 min
Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Re-render scope

Editing one cell of a large grid should re-render one field, not the form. Configured optimally, the modern headless cohort all reaches that bound, and there is no lower number to beat.

Grid: Re-render scope per keystroke

One render per keystroke, whatever the form size. That is the design target, and the run holds it.

LibraryN20M8N100M8
Attaform (Zod 3)
1 renders
1 renders
Attaform (Zod 4)
1 renders
1 renders
vee-validate
1 renders
1 renders
@tanstack/vue-form
1 renders
1 renders
@formisch/vue
1 renders
1 renders
Regle (schema)
1 renders
1 renders
Regle (rules)
1 renders
1 renders
Vuelidate
1 renders
1 renders
FormKit
3 DOM mutations
3 DOM mutations

† Reported as DOM mutations, a proxy for a library that owns its inputs rather than binding the shared bare field. Not directly comparable to a Vue render count.

Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Standing up a form

Mounting a large form

Building a two-thousand-field form from scratch is where the form-state libraries separate. Attaform mounts the whole reactive tree, validation wiring included, every value and validation path live before the first paint.

Massive: Mount

Squarely in the form-state pack, neither out front nor at the back.

LibraryL2000
@formisch/vue
87.2 ms 0.57×
Vuelidate
137 ms 0.89×
Attaform (Zod 3)
154 ms
Attaform (Zod 4)
227 ms 1.48×
Regle (schema)
243 ms 1.58×
Regle (rules)
291 ms 1.89×
vee-validate
951 ms 6.19×
@tanstack/vue-form
10075 ms 65.51×
FormKit
14503 ms 94.3×
Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Memory

Retained heap after mount, the library's reactive and validation state at scenario size. Churn is the per-cycle allocation pressure, and leak is the residual across mount and teardown cycles. The sparkline traces retained heap across the measured cycles.

Massive: Retained heap

Among the faster form-state libraries on this shape.

LibraryL2000
@formisch/vue
11543 kB0.98×
churn 68 kB · leak 3 kB
Attaform (Zod 3)
11740 kB
churn 677 kB · leak 38 kB
Vuelidate
14820 kB1.26×
churn 86 kB · leak 11 kB
Attaform (Zod 4)
21579 kB1.84×
churn 673 kB · leak 63 kB
@tanstack/vue-form
22551 kB1.92×
churn 434 kB · leak 1660 kB
vee-validate
22638 kB1.93×
churn 464 kB · leak 1168 kB
Regle (schema)
33530 kB2.86×
churn 366 kB · leak 24 kB
Regle (rules)
41680 kB3.55×
churn 310 kB · leak 28 kB
FormKit
377186 kB32.13×
churn 115 kB · leak 30 kB
Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Working the harder shapes

Validation throughput

A full-form validation pass over a massive form, the cost of a submit on the largest shape in the suite.

Massive: Full-form validation

Near the front of the form-state pack here.

LibraryL2000L5000
Vuelidate
0.70 ms 0.37×
0.90 ms 0.29×
@formisch/vue
1.40 ms 0.74×
2.90 ms 0.92×
Attaform (Zod 3)
1.90 ms
3.15 ms
Attaform (Zod 4)
1.80 ms 0.95×
3.35 ms 1.06×
vee-validate
16.9 ms 8.89×
35.7 ms 11.33×
Regle (schema)
31.2 ms 16.42×
84.6 ms 26.86×
Regle (rules)
79.0 ms 41.58×
200 ms 63.52×
@tanstack/vue-form
661 ms 347.95×
did not finish> 5 min
FormKit
346 ms 182×
did not finish> 5 min
Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Discriminated unions

Writing into and flipping between variants of a discriminated union. Attaform walks only the active branch, so a variant flip touches the branch in play rather than every alternative.

Discriminated union: Variant flip

Near the front of the form-state pack here.

LibraryDU
Vuelidate
0.50 ms 0.71×
@formisch/vue
0.60 ms 0.86×
Attaform (Zod 3)
0.70 ms
@tanstack/vue-form
0.70 ms
Attaform (Zod 4)
0.80 ms 1.14×
vee-validate
1.00 ms 1.43×
Regle (schema)
1.10 ms 1.57×
Regle (rules)
1.50 ms 2.14×
FormKit
6.00 ms 8.57×
Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Dynamic arrays

Appending and reordering rows in a growing list. Attaform's array helpers copy the target array before mutating, which keeps reads fast and identity stable but shows up as real cost on a reorder at a hundred rows. It is an honest line on the board.

Dynamic arrays: Array append

Mid-pack among the form-state libraries here.

LibraryN10N100
@formisch/vue
0.70 ms 0.47×
1.30 ms 0.45×
Attaform (Zod 4)
1.40 ms 0.93×
2.80 ms 0.97×
Attaform (Zod 3)
1.50 ms
2.90 ms
Vuelidate
1.00 ms 0.67×
3.30 ms 1.14×
@tanstack/vue-form
1.20 ms 0.8×
3.35 ms 1.16×
vee-validate
2.00 ms 1.33×
9.40 ms 3.24×
Regle (rules)
1.60 ms 1.07×
9.90 ms 3.41×
FormKit
4.30 ms 2.87×
10.5 ms 3.62×
Regle (schema)
1.90 ms 1.27×
10.9 ms 3.76×
Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15
Dynamic arrays: Array reorder

Holds the middle of the form-state field on this shape.

LibraryN10N100
@formisch/vue
0.60 ms 0.5×
1.30 ms 0.57×
Vuelidate
0.70 ms 0.58×
1.90 ms 0.83×
Attaform (Zod 4)
1.20 ms
2.20 ms 0.96×
Attaform (Zod 3)
1.20 ms
2.30 ms
@tanstack/vue-form
0.70 ms 0.58×
2.50 ms 1.09×
FormKit
1.20 ms
4.40 ms 1.91×
vee-validate
1.00 ms 0.83×
4.60 ms
Regle (rules)
0.80 ms 0.67×
5.20 ms 2.26×
Regle (schema)
1.00 ms 0.83×
5.80 ms 2.52×
Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Multi-step wizard

Most of the cohort has no wizard primitive and composes a multi-step flow by hand, so this is an expressiveness comparison as much as a timing one. Where a comparable step transition exists, here is its cost; Attaform's useWizard ships the flow as a first-class shape.

Wizard: Step transition

Off the lead here among the form-state libraries, a line we are actively sharpening.

LibraryS4
@formisch/vue
0.10 ms 0.2×
Regle (rules)
0.10 ms 0.2×
Vuelidate
0.10 ms 0.2×
@tanstack/vue-form
0.40 ms 0.8×
Attaform (Zod 4)
0.40 ms 0.8×
Attaform (Zod 3)
0.50 ms
Regle (schema)
0.80 ms 1.6×
FormKit
2.70 ms 5.4×
vee-validate
5.40 ms 10.8×
Source: CI run #27556251045mixed: AMD EPYC 9V74 80-Core Processor + AMD EPYC 7763 64-Core Processor + Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHzNode v24.16.02026-06-15

Caveats

The methodology is only as good as what it admits.

  • FormKit owns its inputs. It cannot drive the shared bare field, so its re-render figure is a DOM-mutation proxy, marked in the tables, and its mount and memory figures include its own component tree. It is labeled batteries-included throughout and never placed silently beside bare-input libraries.
  • Heap is Chromium-quantized. usedJSHeapSize reports rounded magnitudes, not byte-exact values, so memory figures show whole kilobytes and the slope across sizes carries more signal than any single number.
  • Every cell shares one time budget. Each measured cell gets the same per-cell ceiling on the CI runner, identical for every library. A cell that cannot settle to a stable median inside it (a single mount of thousands of fields, or a full-form validation at the largest sizes on the heaviest libraries) is recorded as "did not finish" rather than dropped or estimated. The ceiling is uniform across the cohort, so it marks where a shape outgrows one measurement window, never a verdict on a library.
  • Bundle is total, not first-paint. Every figure is the full minified and gzipped cost with the validator weighed in. Vue is external, since every app ships it once. Code-splitting changes what a first paint actually pulls.
  • An absent score has two distinct meanings. "Not published" means a project has not opted into a Scorecard, which is a choice and not a deficiency. "Unavailable" means the lookup did not complete on that run, a network gap on our side and never a statement about the project. The viewer linked on each row shows the live result either way, and scores are point-in-time.
  • Local versus CI. The committed numbers come from CI on a fixed runner. A figure stamped "local run" is illustrative shape data from a developer machine, superseded the next time CI refreshes the page.

Reproduce it yourself

The harness is meant to be run by hand. Clone the repo, build Attaform's real dist with pnpm prepack, then from apps/bench-arena install the browser and run the arena. The full instructions live in the bench-arena README. Every adapter mounts by query parameter, so you can also open a single library and scenario in a browser and watch it work.

Where to next