[{"data":1,"prerenderedAt":809},["ShallowReactive",2],{"content-\u002Fdocs\u002Fserver-and-ssr\u002Fperformance":3},{"id":4,"title":5,"body":6,"description":789,"extension":790,"meta":791,"metaRows":792,"navigation":481,"path":804,"seo":805,"source":806,"stem":807,"__hash__":808},"docs\u002Fdocs\u002Fserver-and-ssr\u002Fperformance.md","Performance",{"type":7,"value":8,"toc":774},"minimark",[9,13,20,23,37,45,50,57,211,219,223,259,262,266,325,329,357,393,396,404,411,530,533,537,548,554,563,597,600,604,614,618,624,717,727,731,738,742,770],[10,11,5],"h1",{"id":12},"performance",[14,15,16],"blockquote",{},[17,18,19],"p",{},"Notes on the hot paths and what to look at if a form starts feeling slow. Real-browser numbers, sizing guidance, and the array-helper gotcha worth knowing.",[21,22],"docs-meta-table",{},[17,24,25,26,36],{},"This page is reference material; no demo. CI runs the benchmark suite under ",[27,28,32],"a",{"href":29,"rel":30},"https:\u002F\u002Fgithub.com\u002Fattaform\u002FAttaform\u002Ftree\u002Fmain\u002Fbench",[31],"nofollow",[33,34,35],"code",{},"bench\u002F"," on every PR, so the numbers below come from a known-good environment and ride alongside the code.",[17,38,39,40,44],{},"For how Attaform compares across the Vue form-library field on the same scenarios, in a real browser, see ",[27,41,43],{"href":42},"\u002Fdocs\u002Fcomparison\u002Fbenchmarks","Benchmarks",".",[46,47,49],"h2",{"id":48},"measured-numbers","Measured numbers",[17,51,52,53,56],{},"Real-browser numbers from ",[33,54,55],{},"pnpm bench",", single-threaded on contemporary hardware. Your machine will land elsewhere on the number line; the orders of magnitude won't.",[58,59,60,73],"table",{},[61,62,63],"thead",{},[64,65,66,70],"tr",{},[67,68,69],"th",{},"Operation",[67,71,72],{},"Cost",[74,75,76,85,93,105,113,121,129,145,155,163,171,179,187,195,203],"tbody",{},[64,77,78,82],{},[79,80,81],"td",{},"Single-field keystroke, 100-leaf form",[79,83,84],{},"6 µs",[64,86,87,90],{},[79,88,89],{},"Single-field keystroke, 500-leaf form",[79,91,92],{},"30 µs",[64,94,95,102],{},[79,96,97,98,101],{},"Validation overhead per keystroke (",[33,99,100],{},"validateOn: 'change'",")",[79,103,104],{},"5 µs",[64,106,107,110],{},[79,108,109],{},"Submit lifecycle (validate → submit → setErrors)",[79,111,112],{},"3.4 µs",[64,114,115,118],{},[79,116,117],{},"Path canonicalization (cache hit)",[79,119,120],{},"60 ns",[64,122,123,126],{},[79,124,125],{},"Sensitive-name check (common pattern, early hit)",[79,127,128],{},"70 ns",[64,130,131,142],{},[79,132,133,134,137,138,141],{},"Persistence write, ",[33,135,136],{},"'local'"," \u002F ",[33,139,140],{},"'session'"," (100-leaf payload)",[79,143,144],{},"2.8 µs",[64,146,147,152],{},[79,148,133,149,141],{},[33,150,151],{},"'indexeddb'",[79,153,154],{},"62 µs",[64,156,157,160],{},[79,158,159],{},"Debounced-writer schedule (steady-state typing)",[79,161,162],{},"0.18 µs",[64,164,165,168],{},[79,166,167],{},"Discriminated union, write into active variant",[79,169,170],{},"19 µs",[64,172,173,176],{},[79,174,175],{},"Discriminated union, cross-variant flip",[79,177,178],{},"25 µs",[64,180,181,184],{},[79,182,183],{},"Reset, 100-leaf object form",[79,185,186],{},"678 µs",[64,188,189,192],{},[79,190,191],{},"Field-array append, 100-item",[79,193,194],{},"2.5 ms",[64,196,197,200],{},[79,198,199],{},"Field-array append, 1000-item",[79,201,202],{},"9 ms",[64,204,205,208],{},[79,206,207],{},"Field-array swap on 500-item",[79,209,210],{},"3.9 ms",[17,212,213,214,218],{},"A 60 fps frame is ",[215,216,217],"strong",{},"16.7 ms",". Single-keystroke work clears the budget by three orders of magnitude on a 100-leaf form and by two on a 500-leaf form; Vue's render gets the rest of the frame to itself.",[46,220,222],{"id":221},"hot-path-characteristics","Hot-path characteristics",[224,225,226,245,253],"ul",{},[227,228,229,232,233,236,237,244],"li",{},[215,230,231],{},"Keystrokes",": the ",[33,234,235],{},"register"," → form-state path runs against a per-PR threshold; see ",[27,238,241],{"href":239,"rel":240},"https:\u002F\u002Fgithub.com\u002Fattaform\u002FAttaform\u002Fblob\u002Fmain\u002Fbench\u002Fkeystroke.bench.ts",[31],[33,242,243],{},"bench\u002Fkeystroke.bench.ts"," for the measured scenarios (100-leaf and 500-leaf forms, single-leaf mutation).",[227,246,247,252],{},[215,248,249],{},[33,250,251],{},"form.meta.dirty",": iterates the tracked leaves with no per-leaf parse cost.",[227,254,255,258],{},[215,256,257],{},"Path resolution",": dotted-string paths are LRU-cached (128 entries), so repeat canonicalization reduces to a map lookup.",[17,260,261],{},"Sub-500-leaf forms don't surface in profiling.",[46,263,265],{"id":264},"sizing-guidance","Sizing guidance",[58,267,268,278],{},[61,269,270],{},[64,271,272,275],{},[67,273,274],{},"Scale",[67,276,277],{},"Guidance",[74,279,280,288,300],{},[64,281,282,285],{},[79,283,284],{},"≤ 500 leaves",[79,286,287],{},"Default. No tuning needed.",[64,289,290,293],{},[79,291,292],{},"500 – 5,000 leaves",[79,294,295,296,299],{},"Still fine. Watch out for templates that render every leaf's ",[33,297,298],{},"form.fields.\u003Cpath>.dirty"," in a hot scope.",[64,301,302,305],{},[79,303,304],{},"5,000+ leaves",[79,306,307,308,311,312,318,319,44],{},"Consider splitting into sub-forms with distinct ",[33,309,310],{},"key","s, composed via ",[27,313,315],{"href":314},"\u002Fdocs\u002Fcross-cutting-state\u002Finject-form",[33,316,317],{},"injectForm"," or ",[27,320,322],{"href":321},"\u002Fdocs\u002Fmultistep\u002Fuse-wizard",[33,323,324],{},"useWizard",[46,326,328],{"id":327},"array-helpers-are-on","Array helpers are O(N)",[17,330,331,137,334,137,337,137,340,137,343,137,346,349,350,356],{},[33,332,333],{},"append",[33,335,336],{},"prepend",[33,338,339],{},"insert",[33,341,342],{},"remove",[33,344,345],{},"swap",[33,347,348],{},"move"," all copy the target array before mutating. That's cheap in the common case (dozens of items), fine at hundreds, but ",[215,351,352,353,355],{},"quadratic if you loop ",[33,354,333],{}," to seed a large list",". For a large seed, assign the whole array in one shot:",[358,359,364],"pre",{"className":360,"code":361,"language":362,"meta":363,"style":363},"language-ts shiki shiki-themes github-light github-dark","form.setValue('items', preBuiltArray) \u002F\u002F O(N): one allocation\n","ts","",[33,365,366],{"__ignoreMap":363},[367,368,371,375,379,382,386,389],"span",{"class":369,"line":370},"line",1,[367,372,374],{"class":373},"sVt8B","form.",[367,376,378],{"class":377},"sScJk","setValue",[367,380,381],{"class":373},"(",[367,383,385],{"class":384},"sZZnC","'items'",[367,387,388],{"class":373},", preBuiltArray) ",[367,390,392],{"class":391},"sJ8bj","\u002F\u002F O(N): one allocation\n",[17,394,395],{},"For incremental population (the user appends one item at a time), per-append cost is the only thing that matters and the amortized total is linear over the user's interactions.",[46,397,399,400,403],{"id":398},"keying-v-for-rows","Keying ",[33,401,402],{},"v-for"," rows",[17,405,406,407,410],{},"Use a stable per-row key: either an ID carried on the data or a client-generated ",[33,408,409],{},"crypto.randomUUID()"," stored when you append. Keying by index re-renders more than necessary when rows move and flickers focus \u002F scroll state on reordered rows.",[358,412,416],{"className":413,"code":414,"language":415,"meta":363,"style":363},"language-vue shiki shiki-themes github-light github-dark","\u003C!-- Good: stable key follows the item -->\n\u003Cdiv v-for=\"item in form.values.items\" :key=\"item.id\">…\u003C\u002Fdiv>\n\n\u003C!-- Avoid for reorderable lists: index changes when items move -->\n\u003Cdiv v-for=\"(_, i) in form.values.items\" :key=\"i\">…\u003C\u002Fdiv>\n","vue",[33,417,418,423,476,483,489],{"__ignoreMap":363},[367,419,420],{"class":369,"line":370},[367,421,422],{"class":391},"\u003C!-- Good: stable key follows the item -->\n",[367,424,426,429,433,437,440,443,446,449,452,454,457,459,461,463,466,468,471,473],{"class":369,"line":425},2,[367,427,428],{"class":373},"\u003C",[367,430,432],{"class":431},"s9eBZ","div",[367,434,436],{"class":435},"szBVR"," v-for",[367,438,439],{"class":373},"=",[367,441,442],{"class":384},"\"",[367,444,445],{"class":373},"item ",[367,447,448],{"class":435},"in",[367,450,451],{"class":373}," form.values.items",[367,453,442],{"class":384},[367,455,456],{"class":373}," :",[367,458,310],{"class":377},[367,460,439],{"class":373},[367,462,442],{"class":384},[367,464,465],{"class":373},"item.id",[367,467,442],{"class":384},[367,469,470],{"class":373},">…\u003C\u002F",[367,472,432],{"class":431},[367,474,475],{"class":373},">\n",[367,477,479],{"class":369,"line":478},3,[367,480,482],{"emptyLinePlaceholder":481},true,"\n",[367,484,486],{"class":369,"line":485},4,[367,487,488],{"class":391},"\u003C!-- Avoid for reorderable lists: index changes when items move -->\n",[367,490,492,494,496,498,500,502,505,507,509,511,513,515,517,519,522,524,526,528],{"class":369,"line":491},5,[367,493,428],{"class":373},[367,495,432],{"class":431},[367,497,436],{"class":435},[367,499,439],{"class":373},[367,501,442],{"class":384},[367,503,504],{"class":373},"(_, i) ",[367,506,448],{"class":435},[367,508,451],{"class":373},[367,510,442],{"class":384},[367,512,456],{"class":373},[367,514,310],{"class":377},[367,516,439],{"class":373},[367,518,442],{"class":384},[367,520,521],{"class":373},"i",[367,523,442],{"class":384},[367,525,470],{"class":373},[367,527,432],{"class":431},[367,529,475],{"class":373},[17,531,532],{},"The index pattern is fine for append-only or short-lived lists; reach for stable IDs when the list can reorder.",[46,534,536],{"id":535},"discriminated-unions-vs-plain-unions","Discriminated unions vs. plain unions",[17,538,539,540,543,544,547],{},"Discriminated unions (",[33,541,542],{},"z.discriminatedUnion",") walk only the active branch. Plain unions (",[33,545,546],{},"z.union",") walk every branch unconditionally; use a DU when you have a shared key. The cost difference grows with the number of branches; for a 5-branch plain union, validation does 5x the work of the equivalent DU.",[46,549,551,553],{"id":550},"formmetadirty-in-hot-templates",[33,552,251],{}," in hot templates",[17,555,556,558,559,562],{},[33,557,251],{}," is a whole-form aggregate; it invalidates whenever any tracked leaf's ",[33,560,561],{},"updatedAt"," ticks. If you render it in a hot path (a header that re-renders on every keystroke), derive a more specific predicate instead:",[358,564,566],{"className":360,"code":565,"language":362,"meta":363,"style":363},"\u002F\u002F Faster than gating on the whole-form form.meta.dirty:\nconst isEmailDirty = computed(() => form.fields.email.dirty)\n",[33,567,568,573],{"__ignoreMap":363},[367,569,570],{"class":369,"line":370},[367,571,572],{"class":391},"\u002F\u002F Faster than gating on the whole-form form.meta.dirty:\n",[367,574,575,578,582,585,588,591,594],{"class":369,"line":425},[367,576,577],{"class":435},"const",[367,579,581],{"class":580},"sj4cs"," isEmailDirty",[367,583,584],{"class":435}," =",[367,586,587],{"class":377}," computed",[367,589,590],{"class":373},"(() ",[367,592,593],{"class":435},"=>",[367,595,596],{"class":373}," form.fields.email.dirty)\n",[17,598,599],{},"The pattern: read at the smallest granularity that gives you the answer you need.",[46,601,603],{"id":602},"reset-cost","Reset cost",[17,605,606,609,610,613],{},[33,607,608],{},"reset()"," is sub-millisecond on a 100-leaf form (~680 µs in the suite; see the table above). ",[33,611,612],{},"resetField(path)"," scales with the subtree; prefer it for localized reversions.",[46,615,617],{"id":616},"benching-your-own-form","Benching your own form",[17,619,620,621,623],{},"Clone the repo and drop a bench in ",[33,622,35],{},":",[358,625,627],{"className":360,"code":626,"language":362,"meta":363,"style":363},"import { bench, describe } from 'vitest'\nimport { z } from 'zod'\n\u002F\u002F import your form setup\n\ndescribe('my form: typical interaction', () => {\n  bench('the operation I care about', () => {\n    \u002F\u002F ...\n  })\n})\n",[33,628,629,643,655,660,664,682,699,705,711],{"__ignoreMap":363},[367,630,631,634,637,640],{"class":369,"line":370},[367,632,633],{"class":435},"import",[367,635,636],{"class":373}," { bench, describe } ",[367,638,639],{"class":435},"from",[367,641,642],{"class":384}," 'vitest'\n",[367,644,645,647,650,652],{"class":369,"line":425},[367,646,633],{"class":435},[367,648,649],{"class":373}," { z } ",[367,651,639],{"class":435},[367,653,654],{"class":384}," 'zod'\n",[367,656,657],{"class":369,"line":478},[367,658,659],{"class":391},"\u002F\u002F import your form setup\n",[367,661,662],{"class":369,"line":485},[367,663,482],{"emptyLinePlaceholder":481},[367,665,666,669,671,674,677,679],{"class":369,"line":491},[367,667,668],{"class":377},"describe",[367,670,381],{"class":373},[367,672,673],{"class":384},"'my form: typical interaction'",[367,675,676],{"class":373},", () ",[367,678,593],{"class":435},[367,680,681],{"class":373}," {\n",[367,683,685,688,690,693,695,697],{"class":369,"line":684},6,[367,686,687],{"class":377},"  bench",[367,689,381],{"class":373},[367,691,692],{"class":384},"'the operation I care about'",[367,694,676],{"class":373},[367,696,593],{"class":435},[367,698,681],{"class":373},[367,700,702],{"class":369,"line":701},7,[367,703,704],{"class":391},"    \u002F\u002F ...\n",[367,706,708],{"class":369,"line":707},8,[367,709,710],{"class":373},"  })\n",[367,712,714],{"class":369,"line":713},9,[367,715,716],{"class":373},"})\n",[17,718,719,720,722,723,726],{},"Run with ",[33,721,55],{},". The regression gate only fires on benches that follow the ",[33,724,725],{},"old: \u002F new:"," pairing convention; informational benches run without gating.",[46,728,730],{"id":729},"peer-dep-coverage","Peer-dep coverage",[17,732,733,734,737],{},"Per-PR CI runs the suite on the current Node LTS (22.x today; the ",[33,735,736],{},"engines.node"," floor is Node 22) against the devDep-pinned peer versions. A weekly workflow sweeps Vue 3.5 through 3.6, Vite 5 \u002F 6, Nuxt 3.16 through Nuxt 4. Jobs fail independently; versions not yet released surface as failed cells without blocking the main CI.",[46,739,741],{"id":740},"where-to-next","Where to next",[224,743,744,749,756,763],{},[227,745,746,748],{},[27,747,43],{"href":42},": the same scenarios run across the Vue form-library field, with bundle size, supply-chain scores, and per-scenario runtime tables.",[227,750,751,755],{},[27,752,754],{"href":753},"\u002Fdocs\u002Fwriting-and-mutating\u002Ffield-arrays","Field-array mutations",": the O(N) characteristics in full, including amortized analysis.",[227,757,758,762],{},[27,759,761],{"href":760},"\u002Fdocs\u002Fschemas\u002Fstorage-shape","How values are stored",": the slim write shape that keeps reads fast.",[227,764,765,769],{},[27,766,768],{"href":767},"\u002Fdocs\u002Fserver-and-ssr\u002Fssr-nuxt","SSR hydration: Nuxt",": hydration costs depend on form size; pair this page with the SSR pages when sizing.",[771,772,773],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":363,"searchDepth":425,"depth":425,"links":775},[776,777,778,779,780,782,783,785,786,787,788],{"id":48,"depth":425,"text":49},{"id":221,"depth":425,"text":222},{"id":264,"depth":425,"text":265},{"id":327,"depth":425,"text":328},{"id":398,"depth":425,"text":781},"Keying v-for rows",{"id":535,"depth":425,"text":536},{"id":550,"depth":425,"text":784},"form.meta.dirty in hot templates",{"id":602,"depth":425,"text":603},{"id":616,"depth":425,"text":617},{"id":729,"depth":425,"text":730},{"id":740,"depth":425,"text":741},"Measured numbers for keystrokes, validation, submit, persistence, and the array helpers. Sub-500-leaf forms don't surface in profiling; the patterns to watch on bigger forms.","md",{},[793,796,799,801],{"label":794,"value":795},"Category","Reference",{"label":797,"value":798},"Default","no tuning under 500 leaves",{"label":800,"value":292},"Sweet spot",{"label":802,"value":803},"Frame budget","16.7 ms @ 60 fps","\u002Fdocs\u002Fserver-and-ssr\u002Fperformance",{"title":5,"description":789},null,"docs\u002Fserver-and-ssr\u002Fperformance","qjVUpzAkdlqIAnoF2Z5jiQqCnt7wsO4FlunZBfPmNIM",1781745868957]