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