[{"data":1,"prerenderedAt":1316},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-vue-event-handlers":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-vue-event-handlers":1307},{"id":4,"title":5,"body":6,"description":1287,"extension":1288,"meta":1289,"navigation":605,"path":1301,"seo":1302,"stem":1305,"__hash__":1306},"content\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-vue-event-handlers\u002Findex.md","Breaking Up Long Tasks in Vue Event Handlers",{"type":7,"value":8,"toc":1272},"minimark",[9,14,34,37,201,206,263,267,273,279,287,293,381,385,390,393,527,535,539,542,711,714,718,721,903,910,914,921,1015,1018,1102,1106,1115,1119,1129,1144,1164,1173,1177,1196,1200,1212,1221,1230,1234,1257,1262,1265,1268],[10,11,13],"h1",{"id":12},"how-to-break-up-long-tasks-in-vue-event-handlers","How to Break Up Long Tasks in Vue Event Handlers",[15,16,17,18,23,24,28,29,33],"p",{},"This guide is the Vue counterpart to ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002F","breaking up long tasks in React event handlers",", within ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002F","Optimizing INP with scheduler.yield()"," and ",[19,30,32],{"href":31},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",". Vue's reactivity system makes state changes feel cheap — assign a value and the UI updates — which hides how much work a single assignment can trigger.",[15,35,36],{},"When a handler mutates reactive state, Vue queues every affected computed property, watcher and component render, then flushes the queue in a microtask after the handler returns. All of that runs before the browser can paint. A click that sets one filter value can recompute a computed over 5,000 items, run several watchers that call APIs, and re-render a large list — one long task from the browser's perspective, even if the handler itself is three lines.",[15,38,39],{},[40,41,47,48,47,55,47,59,47,62,47,80,47,88,47,94,47,102,47,109,47,114,47,118,47,124,47,128,47,131,47,134,47,137,47,141,47,144,47,147,47,150,47,154,47,157,47,160,47,163,47,167,47,170,47,179,47,183,47,187,47,191,47,197,47],"svg",{"viewBox":42,"width":43,"role":44,"ariaLabel":45,"style":46},"0 0 760 193","100%","img","The chain of work Vue performs in one microtask flush after a handler mutates reactive state.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[49,50],"rect",{"className":51,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],"svg-canvas","0","#ffffff",[56,57,58],"title",{},"What one assignment triggers in Vue",[60,61,45],"desc",{},[63,64,65],"defs",{},[66,67,74],"marker",{"id":68,"viewBox":69,"refX":70,"refY":71,"markerWidth":72,"markerHeight":72,"orient":73},"fab1477fba","0 0 10 10","9","5","7","auto-start-reverse",[75,76],"path",{"d":77,"fill":78,"style":79},"M0 0 L10 5 L0 10 z","currentColor","fill-opacity:0.7",[49,81],{"x":82,"y":82,"width":83,"height":84,"rx":85,"fill":86,"stroke":78,"style":87},"1","758","191","10","none","stroke-opacity:0.18",[89,90,58],"text",{"x":91,"y":92,"fill":78,"style":93},"28.0","34.0","font-size:16px;font-weight:700",[49,95],{"x":91,"y":96,"width":97,"height":98,"rx":99,"fill":100,"stroke":100,"style":101},"56.0","108.8","71.0","6","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[89,103,108],{"x":104,"y":105,"fill":78,"style":106,"textAnchor":107},"82.4","78.0","font-size:13px;font-weight:700","middle","Handler",[89,110,113],{"x":104,"y":111,"fill":78,"style":112,"textAnchor":107},"96.0","font-size:12px","filters.color =",[89,115,117],{"x":104,"y":116,"fill":78,"style":112,"textAnchor":107},"112.0","'red'",[49,119],{"x":120,"y":96,"width":97,"height":98,"rx":99,"fill":121,"stroke":122,"style":123},"176.8","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[89,125,127],{"x":126,"y":105,"fill":78,"style":106,"textAnchor":107},"231.2","Computed",[89,129,130],{"x":126,"y":111,"fill":78,"style":112,"textAnchor":107},"re-filter 5,000",[89,132,133],{"x":126,"y":116,"fill":78,"style":112,"textAnchor":107},"items",[49,135],{"x":136,"y":96,"width":97,"height":98,"rx":99,"fill":100,"stroke":100,"style":101},"325.6",[89,138,140],{"x":139,"y":105,"fill":78,"style":106,"textAnchor":107},"380.0","Watchers",[89,142,143],{"x":139,"y":111,"fill":78,"style":112,"textAnchor":107},"pre-flush",[89,145,146],{"x":139,"y":116,"fill":78,"style":112,"textAnchor":107},"callbacks",[49,148],{"x":149,"y":96,"width":97,"height":98,"rx":99,"fill":121,"stroke":122,"style":123},"474.4",[89,151,153],{"x":152,"y":105,"fill":78,"style":106,"textAnchor":107},"528.8","Render",[89,155,156],{"x":152,"y":111,"fill":78,"style":112,"textAnchor":107},"patch large",[89,158,159],{"x":152,"y":116,"fill":78,"style":112,"textAnchor":107},"v-for",[49,161],{"x":162,"y":96,"width":97,"height":98,"rx":99,"fill":100,"stroke":100,"style":101},"623.2",[89,164,166],{"x":165,"y":105,"fill":78,"style":106,"textAnchor":107},"677.6","Paint",[89,168,169],{"x":165,"y":111,"fill":78,"style":112,"textAnchor":107},"after the flush",[171,172],"line",{"x1":173,"y1":174,"x2":175,"y2":174,"stroke":78,"strokeWidth":176,"style":177,"markerEnd":178},"136.8","91.5","174.8","1.5","stroke-opacity:0.6","url(#fab1477fba)",[171,180],{"x1":181,"y1":174,"x2":182,"y2":174,"stroke":78,"strokeWidth":176,"style":177,"markerEnd":178},"285.6","323.6",[171,184],{"x1":185,"y1":174,"x2":186,"y2":174,"stroke":78,"strokeWidth":176,"style":177,"markerEnd":178},"434.4","472.4",[171,188],{"x1":189,"y1":174,"x2":190,"y2":174,"stroke":78,"strokeWidth":176,"style":177,"markerEnd":178},"583.2","621.2",[89,192,196],{"x":91,"y":193,"fill":194,"style":195},"153.0","#51617a","font-size:12.5px","Steps two to four run in one microtask flush after the handler returns, so the browser cannot paint or handle input",[89,198,200],{"x":91,"y":199,"fill":194,"style":195},"171.0","until all of them finish.",[202,203,205],"h2",{"id":204},"rapid-diagnosis","Rapid Diagnosis",[207,208,209,217,235,241,250],"ul",{},[210,211,212,216],"li",{},[213,214,215],"strong",{},"Profile with Vue DevTools' Performance tab"," (or the Timeline) while performing the interaction. Note component render times and how many components re-rendered.",[210,218,219,222,223,227,228,28,231,234],{},[213,220,221],{},"In the Chrome Performance panel",", expand the interaction's long task: look for ",[224,225,226],"code",{},"flushJobs",", ",[224,229,230],{},"flushPreFlushCbs",[224,232,233],{},"componentUpdateFn"," in the call tree, and the heaviest computed getters below them.",[210,236,237,240],{},[213,238,239],{},"Count reactive dependencies."," A computed that iterates a deep reactive array tracks every element's every accessed property; reading it after a change re-runs everything.",[210,242,243,47,246,249],{},[213,244,245],{},"Check for deep watchers.",[224,247,248],{},"watch(source, cb, { deep: true })"," on large objects traverses the whole structure on every change.",[210,251,252,255,256,258,259,262],{},[213,253,254],{},"Check list keys."," A ",[224,257,159],{}," without stable ",[224,260,261],{},":key"," values re-creates rows instead of patching them.",[202,264,266],{"id":265},"root-cause-analysis","Root Cause Analysis",[15,268,269,272],{},[213,270,271],{},"1. Expensive computed properties on large reactive data."," Deep reactivity wraps every nested object in a proxy; filtering and sorting through proxies is markedly slower than through plain arrays.",[15,274,275,278],{},[213,276,277],{},"2. Deep watchers and watcher chains."," One change triggers a watcher that mutates more state, triggering more watchers — all inside the same flush.",[15,280,281,255,284,286],{},[213,282,283],{},"3. Large list re-renders.",[224,285,159],{}," over hundreds of items re-renders when its source array is replaced, even if most items are unchanged.",[15,288,289,292],{},[213,290,291],{},"4. Synchronous work in the handler itself."," Building payloads, validating forms or transforming data before mutating state.",[15,294,295],{},[40,296,47,299,47,302,47,305,47,307,47,310,47,312,47,320,47,327,47,334,47,339,47,343,47,348,47,352,47,355,47,360,47,364,47,368,47,371,47,375,47],{"viewBox":297,"width":43,"role":44,"ariaLabel":298,"style":46},"0 0 760 216","Bar chart splitting a slow Vue filter interaction into handler, computed filtering, watcher work and list re-render.",[49,300],{"className":301,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,303,304],{},"A Vue filter click on a 5,000-product catalogue",[60,306,298],{},[49,308],{"x":82,"y":82,"width":83,"height":309,"rx":85,"fill":86,"stroke":78,"style":87},"214",[89,311,304],{"x":91,"y":92,"fill":78,"style":93},[171,313],{"x1":314,"y1":315,"x2":314,"y2":316,"stroke":122,"strokeWidth":176,"strokeDashArray":317,"style":319},"402.8","72.0","194.0",[318,318],"4","stroke-opacity:0.9",[89,321,326],{"x":322,"y":323,"fill":78,"style":324,"textAnchor":325},"177.0","92.0","font-size:13px","end","Handler body",[49,328],{"x":329,"y":105,"width":330,"height":331,"rx":332,"fill":78,"stroke":78,"style":333},"189.0","17.1","19","3","fill-opacity:0.7;stroke-opacity:0.4",[89,335,338],{"x":336,"y":323,"fill":78,"style":337},"212.1","font-size:12px;font-weight:600","4ms",[89,340,342],{"x":322,"y":341,"fill":78,"style":324,"textAnchor":325},"123.0","Computed (deep reactive)",[49,344],{"x":329,"y":345,"width":346,"height":331,"rx":332,"fill":121,"stroke":122,"style":347},"109.0","410.6","fill-opacity:0.7;stroke-opacity:0.9",[89,349,351],{"x":350,"y":341,"fill":78,"style":337},"605.6","96ms",[89,353,140],{"x":322,"y":354,"fill":78,"style":324,"textAnchor":325},"154.0",[49,356],{"x":329,"y":357,"width":358,"height":331,"rx":332,"fill":100,"stroke":100,"style":359},"140.0","119.8","fill-opacity:0.55;stroke-opacity:0.9",[89,361,363],{"x":362,"y":354,"fill":78,"style":337},"314.7","28ms",[89,365,367],{"x":322,"y":366,"fill":78,"style":324,"textAnchor":325},"185.0","v-for re-render (300 rows)",[49,369],{"x":329,"y":199,"width":370,"height":331,"rx":332,"fill":121,"stroke":122,"style":347},"479.0",[89,372,374],{"x":373,"y":366,"fill":78,"style":337},"674.0","112ms",[89,376,380],{"x":314,"y":377,"fill":378,"style":379,"textAnchor":107},"68.0","#8a6608","font-size:12px;font-weight:700","50ms task budget",[202,382,384],{"id":383},"step-by-step-resolution","Step-by-Step Resolution",[386,387,389],"h3",{"id":388},"_1-store-large-datasets-with-shallowref","1. Store large datasets with shallowRef",[15,391,392],{},"Large arrays of records that you replace rather than mutate in place should not be deeply reactive.",[394,395,400],"pre",{"className":396,"code":397,"language":398,"meta":399,"style":399},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","import { shallowRef, computed } from 'vue';\nconst products = shallowRef([]);                 \u002F\u002F only .value replacement is tracked\nconst filtered = computed(() =>\n  products.value.filter((p) => p.color === filters.color && p.price \u003C= filters.maxPrice));\n\u002F\u002F When data changes, replace the array: products.value = newArray\n\u002F\u002F trade-off: in-place mutations (products.value.push(x)) do not trigger\n\u002F\u002F updates with shallowRef. Replace the array, or call triggerRef(products).\n","javascript","",[224,401,402,424,448,467,509,515,521],{"__ignoreMap":399},[403,404,406,410,414,417,421],"span",{"class":171,"line":405},1,[403,407,409],{"class":408},"sPARh","import",[403,411,413],{"class":412},"saISM"," { shallowRef, computed } ",[403,415,416],{"class":408},"from",[403,418,420],{"class":419},"sZ8jY"," 'vue'",[403,422,423],{"class":412},";\n",[403,425,427,430,434,437,441,444],{"class":171,"line":426},2,[403,428,429],{"class":408},"const",[403,431,433],{"class":432},"sPXB4"," products",[403,435,436],{"class":408}," =",[403,438,440],{"class":439},"smZ65"," shallowRef",[403,442,443],{"class":412},"([]);                 ",[403,445,447],{"class":446},"sjfSM","\u002F\u002F only .value replacement is tracked\n",[403,449,451,453,456,458,461,464],{"class":171,"line":450},3,[403,452,429],{"class":408},[403,454,455],{"class":432}," filtered",[403,457,436],{"class":408},[403,459,460],{"class":439}," computed",[403,462,463],{"class":412},"(() ",[403,465,466],{"class":408},"=>\n",[403,468,470,473,476,479,482,485,488,491,494,497,500,503,506],{"class":171,"line":469},4,[403,471,472],{"class":412},"  products.value.",[403,474,475],{"class":439},"filter",[403,477,478],{"class":412},"((",[403,480,15],{"class":481},"sQw3B",[403,483,484],{"class":412},") ",[403,486,487],{"class":408},"=>",[403,489,490],{"class":412}," p.color ",[403,492,493],{"class":408},"===",[403,495,496],{"class":412}," filters.color ",[403,498,499],{"class":408},"&&",[403,501,502],{"class":412}," p.price ",[403,504,505],{"class":408},"\u003C=",[403,507,508],{"class":412}," filters.maxPrice));\n",[403,510,512],{"class":171,"line":511},5,[403,513,514],{"class":446},"\u002F\u002F When data changes, replace the array: products.value = newArray\n",[403,516,518],{"class":171,"line":517},6,[403,519,520],{"class":446},"\u002F\u002F trade-off: in-place mutations (products.value.push(x)) do not trigger\n",[403,522,524],{"class":171,"line":523},7,[403,525,526],{"class":446},"\u002F\u002F updates with shallowRef. Replace the array, or call triggerRef(products).\n",[15,528,529,530,534],{},"Expected outcome: computed filtering over plain objects runs several times faster; on large catalogues, tens of milliseconds saved. See ",[19,531,533],{"href":532},"\u002Fframework-performance\u002Fvue-and-nuxt-performance\u002Fcutting-vue-reactivity-cost-with-shallowref\u002F","cutting Vue reactivity cost with shallowRef",".",[386,536,538],{"id":537},"_2-yield-before-the-expensive-state-change","2. Yield before the expensive state change",[15,540,541],{},"Let the browser paint the pressed state and handle pending input before triggering the heavy flush.",[394,543,545],{"className":396,"code":544,"language":398,"meta":399,"style":399},"const yieldToMain = () =>\n  globalThis.scheduler?.yield?.() ?? new Promise((r) => setTimeout(r, 0));\n\nasync function onApplyFilters() {\n  applying.value = true;                         \u002F\u002F cheap: shows a spinner on the button\n  await yieldToMain();                           \u002F\u002F paint happens here\n  filters.color = pendingColor.value;            \u002F\u002F heavy reactive flush in the next task\n  await nextTick();\n  applying.value = false;\n}\n\u002F\u002F trade-off: the results now update in a task after the interaction's paint,\n\u002F\u002F so INP measures only the cheap first part. That is the goal — but make sure\n\u002F\u002F the pending state is visible so users do not click again.\n",[224,546,547,561,601,607,621,638,651,664,675,687,693,699,705],{"__ignoreMap":399},[403,548,549,551,554,556,559],{"class":171,"line":405},[403,550,429],{"class":408},[403,552,553],{"class":439}," yieldToMain",[403,555,436],{"class":408},[403,557,558],{"class":412}," () ",[403,560,466],{"class":408},[403,562,563,566,569,572,575,578,581,583,586,588,590,593,596,598],{"class":171,"line":426},[403,564,565],{"class":412},"  globalThis.scheduler?.",[403,567,568],{"class":439},"yield",[403,570,571],{"class":412},"?.() ",[403,573,574],{"class":408},"??",[403,576,577],{"class":408}," new",[403,579,580],{"class":432}," Promise",[403,582,478],{"class":412},[403,584,585],{"class":481},"r",[403,587,484],{"class":412},[403,589,487],{"class":408},[403,591,592],{"class":439}," setTimeout",[403,594,595],{"class":412},"(r, ",[403,597,53],{"class":432},[403,599,600],{"class":412},"));\n",[403,602,603],{"class":171,"line":450},[403,604,606],{"emptyLinePlaceholder":605},true,"\n",[403,608,609,612,615,618],{"class":171,"line":469},[403,610,611],{"class":408},"async",[403,613,614],{"class":408}," function",[403,616,617],{"class":439}," onApplyFilters",[403,619,620],{"class":412},"() {\n",[403,622,623,626,629,632,635],{"class":171,"line":511},[403,624,625],{"class":412},"  applying.value ",[403,627,628],{"class":408},"=",[403,630,631],{"class":432}," true",[403,633,634],{"class":412},";                         ",[403,636,637],{"class":446},"\u002F\u002F cheap: shows a spinner on the button\n",[403,639,640,643,645,648],{"class":171,"line":517},[403,641,642],{"class":408},"  await",[403,644,553],{"class":439},[403,646,647],{"class":412},"();                           ",[403,649,650],{"class":446},"\u002F\u002F paint happens here\n",[403,652,653,656,658,661],{"class":171,"line":523},[403,654,655],{"class":412},"  filters.color ",[403,657,628],{"class":408},[403,659,660],{"class":412}," pendingColor.value;            ",[403,662,663],{"class":446},"\u002F\u002F heavy reactive flush in the next task\n",[403,665,667,669,672],{"class":171,"line":666},8,[403,668,642],{"class":408},[403,670,671],{"class":439}," nextTick",[403,673,674],{"class":412},"();\n",[403,676,678,680,682,685],{"class":171,"line":677},9,[403,679,625],{"class":412},[403,681,628],{"class":408},[403,683,684],{"class":432}," false",[403,686,423],{"class":412},[403,688,690],{"class":171,"line":689},10,[403,691,692],{"class":412},"}\n",[403,694,696],{"class":171,"line":695},11,[403,697,698],{"class":446},"\u002F\u002F trade-off: the results now update in a task after the interaction's paint,\n",[403,700,702],{"class":171,"line":701},12,[403,703,704],{"class":446},"\u002F\u002F so INP measures only the cheap first part. That is the goal — but make sure\n",[403,706,708],{"class":171,"line":707},13,[403,709,710],{"class":446},"\u002F\u002F the pending state is visible so users do not click again.\n",[15,712,713],{},"Expected outcome: the interaction's processing time falls to the cost of the first state change; the heavy flush no longer counts towards INP for that click.",[386,715,717],{"id":716},"_3-chunk-large-recomputations","3. Chunk large recomputations",[15,719,720],{},"When the derived data is genuinely expensive, compute it in chunks outside the reactive system and assign the result once.",[394,722,724],{"className":396,"code":723,"language":398,"meta":399,"style":399},"async function recompute(all, predicate) {\n  const out = [];\n  let deadline = performance.now() + 40;\n  for (const p of all) {\n    if (predicate(p)) out.push(p);\n    if (performance.now() > deadline) { await yieldToMain(); deadline = performance.now() + 40; }\n  }\n  results.value = out;                           \u002F\u002F single reactive assignment\n}\n\u002F\u002F trade-off: computed properties cannot be async, so chunked derivations live\n\u002F\u002F in functions you call explicitly. You lose automatic dependency tracking for\n\u002F\u002F that value; call recompute() from a watcher on the inputs instead.\n",[224,725,726,749,762,789,808,826,866,871,884,888,893,898],{"__ignoreMap":399},[403,727,728,730,732,735,738,741,743,746],{"class":171,"line":405},[403,729,611],{"class":408},[403,731,614],{"class":408},[403,733,734],{"class":439}," recompute",[403,736,737],{"class":412},"(",[403,739,740],{"class":481},"all",[403,742,227],{"class":412},[403,744,745],{"class":481},"predicate",[403,747,748],{"class":412},") {\n",[403,750,751,754,757,759],{"class":171,"line":426},[403,752,753],{"class":408},"  const",[403,755,756],{"class":432}," out",[403,758,436],{"class":408},[403,760,761],{"class":412}," [];\n",[403,763,764,767,770,772,775,778,781,784,787],{"class":171,"line":450},[403,765,766],{"class":408},"  let",[403,768,769],{"class":412}," deadline ",[403,771,628],{"class":408},[403,773,774],{"class":412}," performance.",[403,776,777],{"class":439},"now",[403,779,780],{"class":412},"() ",[403,782,783],{"class":408},"+",[403,785,786],{"class":432}," 40",[403,788,423],{"class":412},[403,790,791,794,797,799,802,805],{"class":171,"line":469},[403,792,793],{"class":408},"  for",[403,795,796],{"class":412}," (",[403,798,429],{"class":408},[403,800,801],{"class":432}," p",[403,803,804],{"class":408}," of",[403,806,807],{"class":412}," all) {\n",[403,809,810,813,815,817,820,823],{"class":171,"line":511},[403,811,812],{"class":408},"    if",[403,814,796],{"class":412},[403,816,745],{"class":439},[403,818,819],{"class":412},"(p)) out.",[403,821,822],{"class":439},"push",[403,824,825],{"class":412},"(p);\n",[403,827,828,830,833,835,837,840,843,846,848,851,853,855,857,859,861,863],{"class":171,"line":517},[403,829,812],{"class":408},[403,831,832],{"class":412}," (performance.",[403,834,777],{"class":439},[403,836,780],{"class":412},[403,838,839],{"class":408},">",[403,841,842],{"class":412}," deadline) { ",[403,844,845],{"class":408},"await",[403,847,553],{"class":439},[403,849,850],{"class":412},"(); deadline ",[403,852,628],{"class":408},[403,854,774],{"class":412},[403,856,777],{"class":439},[403,858,780],{"class":412},[403,860,783],{"class":408},[403,862,786],{"class":432},[403,864,865],{"class":412},"; }\n",[403,867,868],{"class":171,"line":523},[403,869,870],{"class":412},"  }\n",[403,872,873,876,878,881],{"class":171,"line":666},[403,874,875],{"class":412},"  results.value ",[403,877,628],{"class":408},[403,879,880],{"class":412}," out;                           ",[403,882,883],{"class":446},"\u002F\u002F single reactive assignment\n",[403,885,886],{"class":171,"line":677},[403,887,692],{"class":412},[403,889,890],{"class":171,"line":689},[403,891,892],{"class":446},"\u002F\u002F trade-off: computed properties cannot be async, so chunked derivations live\n",[403,894,895],{"class":171,"line":695},[403,896,897],{"class":446},"\u002F\u002F in functions you call explicitly. You lose automatic dependency tracking for\n",[403,899,900],{"class":171,"line":701},[403,901,902],{"class":446},"\u002F\u002F that value; call recompute() from a watcher on the inputs instead.\n",[15,904,905,906,909],{},"Expected outcome: no task exceeds ",[224,907,908],{},"50ms"," during recomputation.",[386,911,913],{"id":912},"_4-reduce-what-re-renders","4. Reduce what re-renders",[15,915,916,917,920],{},"Use stable keys, ",[224,918,919],{},"v-memo"," for rows whose inputs are unchanged, and render only visible rows for long lists.",[394,922,926],{"className":923,"code":924,"language":925,"meta":399,"style":399},"language-vue shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u003Cdiv v-for=\"p in visibleProducts\" :key=\"p.id\" v-memo=\"[p.id, p.price, selectedId === p.id]\">\n  \u003CProductRow :product=\"p\" :selected=\"selectedId === p.id\" \u002F>\n\u003C\u002Fdiv>\n\u003C!-- trade-off: v-memo skips updates unless a listed dependency changes. Forget a\n     dependency and the row shows stale data — list every value the row reads. -->\n","vue",[224,927,928,991,996,1005,1010],{"__ignoreMap":399},[403,929,930,933,937,940,942,945,948,951,954,956,959,962,964,966,969,971,974,976,978,981,983,986,988],{"class":171,"line":405},[403,931,932],{"class":412},"\u003C",[403,934,936],{"class":935},"sZBmE","div",[403,938,939],{"class":408}," v-for",[403,941,628],{"class":412},[403,943,944],{"class":412},"\"",[403,946,947],{"class":412},"p ",[403,949,950],{"class":408},"in",[403,952,953],{"class":412}," visibleProducts",[403,955,944],{"class":412},[403,957,958],{"class":412}," :",[403,960,961],{"class":432},"key",[403,963,628],{"class":412},[403,965,944],{"class":412},[403,967,968],{"class":412},"p.id",[403,970,944],{"class":412},[403,972,973],{"class":432}," v-memo",[403,975,628],{"class":412},[403,977,944],{"class":412},[403,979,980],{"class":412},"[p.id, p.price, selectedId ",[403,982,493],{"class":408},[403,984,985],{"class":412}," p.id]",[403,987,944],{"class":412},[403,989,990],{"class":412},">\n",[403,992,993],{"class":171,"line":426},[403,994,995],{"class":412},"  \u003CProductRow :product=\"p\" :selected=\"selectedId === p.id\" \u002F>\n",[403,997,998,1001,1003],{"class":171,"line":450},[403,999,1000],{"class":412},"\u003C\u002F",[403,1002,936],{"class":935},[403,1004,990],{"class":412},[403,1006,1007],{"class":171,"line":469},[403,1008,1009],{"class":446},"\u003C!-- trade-off: v-memo skips updates unless a listed dependency changes. Forget a\n",[403,1011,1012],{"class":171,"line":511},[403,1013,1014],{"class":446},"     dependency and the row shows stale data — list every value the row reads. -->\n",[15,1016,1017],{},"Expected outcome: the re-render touches only changed rows; on lists of hundreds of items this is often a 3–10x reduction in render time.",[15,1019,1020],{},[40,1021,47,1024,47,1027,47,1030,47,1032,47,1035,47,1037,47,1041,47,1047,47,1052,47,1056,47,1059,47,1062,47,1065,47,1068,47,1071,47,1074,47,1077,47,1081,47,1083,47,1087,47,1089,47,1092,47,1094,47,1097,47,1099,47],{"viewBox":1022,"width":43,"role":44,"ariaLabel":1023,"style":46},"0 0 760 228","Comparison of a Vue click handler that triggers a single long flush with a restructured handler that paints a pending state and then applies chunked work.",[49,1025],{"className":1026,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,1028,1029],{},"Handler shape before and after",[60,1031,1023],{},[49,1033],{"x":82,"y":82,"width":83,"height":1034,"rx":85,"fill":86,"stroke":78,"style":87},"226",[89,1036,1029],{"x":91,"y":92,"fill":78,"style":93},[49,1038],{"x":91,"y":96,"width":1039,"height":1040,"rx":99,"fill":121,"stroke":122,"style":123},"340.0","150.0",[89,1042,1046],{"x":1043,"y":1044,"fill":78,"style":1045},"42.0","82.0","font-size:14px;font-weight:700","One long flush (240ms)",[89,1048,1051],{"x":1043,"y":1049,"fill":78,"style":1050},"108.0","font-size:12.5px;font-weight:700","•",[89,1053,1055],{"x":96,"y":1049,"fill":78,"style":195,"textAnchor":1054},"start","Assign filter value in handler",[89,1057,1051],{"x":1043,"y":1058,"fill":78,"style":1050},"132.0",[89,1060,1061],{"x":96,"y":1058,"fill":78,"style":195,"textAnchor":1054},"Deep computed re-filters 5,000 proxies",[89,1063,1051],{"x":1043,"y":1064,"fill":78,"style":1050},"156.0",[89,1066,1067],{"x":96,"y":1064,"fill":78,"style":195,"textAnchor":1054},"Watchers + full list patch",[89,1069,1051],{"x":1043,"y":1070,"fill":78,"style":1050},"180.0",[89,1072,1073],{"x":96,"y":1070,"fill":78,"style":195,"textAnchor":1054},"INP 260ms",[49,1075],{"x":1076,"y":96,"width":1039,"height":1040,"rx":99,"fill":100,"stroke":100,"style":101},"392.0",[89,1078,1080],{"x":1079,"y":1044,"fill":78,"style":1045},"406.0","Paint first, chunk the rest",[89,1082,1051],{"x":1079,"y":1049,"fill":78,"style":1050},[89,1084,1086],{"x":1085,"y":1049,"fill":78,"style":195,"textAnchor":1054},"420.0","Show pending state, yield",[89,1088,1051],{"x":1079,"y":1058,"fill":78,"style":1050},[89,1090,1091],{"x":1085,"y":1058,"fill":78,"style":195,"textAnchor":1054},"Chunked filter over shallowRef data",[89,1093,1051],{"x":1079,"y":1064,"fill":78,"style":1050},[89,1095,1096],{"x":1085,"y":1064,"fill":78,"style":195,"textAnchor":1054},"v-memo rows, visible items only",[89,1098,1051],{"x":1079,"y":1070,"fill":78,"style":1050},[89,1100,1101],{"x":1085,"y":1070,"fill":78,"style":195,"textAnchor":1054},"INP 70ms, results 180ms later",[202,1103,1105],{"id":1104},"verification","Verification",[15,1107,1108,1109,1111,1112,534],{},"Record the interaction again with 4x CPU throttling. The interaction's long task should be gone; instead you should see a short task for the click, a paint, then several sub-",[224,1110,908],{}," tasks for the recomputation and render. In Vue DevTools, the number of components re-rendered should drop. In RUM, INP attribution for the filter control should show processing duration well below ",[224,1113,1114],{},"100ms",[202,1116,1118],{"id":1117},"vue-specific-traps-when-yielding","Vue-Specific Traps When Yielding",[15,1120,1121,1124,1125,1128],{},[213,1122,1123],{},"Reading reactive state across an await."," After ",[224,1126,1127],{},"await yieldToMain()",", other code may have changed state. Re-read what you need after the yield instead of relying on values captured before it.",[15,1130,1131,1137,1138,1140,1141,1143],{},[213,1132,1133,1136],{},[224,1134,1135],{},"watchEffect"," with async bodies."," Dependencies are only tracked before the first ",[224,1139,845],{},". Code after a yield inside a ",[224,1142,1135],{}," will not re-run when its dependencies change. Keep tracked reads synchronous at the top.",[15,1145,1146,47,1152,1155,1156,1159,1160,1163],{},[213,1147,1148,1151],{},[224,1149,1150],{},"nextTick"," is not a yield.",[224,1153,1154],{},"nextTick()"," waits for Vue's own flush, which happens in a microtask — the browser cannot paint or handle input in between. Use a real task boundary (",[224,1157,1158],{},"scheduler.yield()"," or ",[224,1161,1162],{},"setTimeout",") when the goal is to let the browser breathe.",[15,1165,1166,1169,1170,1172],{},[213,1167,1168],{},"Pinia actions."," Store actions often contain the expensive logic. They can be ",[224,1171,611],{}," and yield like any function; make sure components awaiting them render a pending state rather than blocking on the result.",[202,1174,1176],{"id":1175},"measuring-vue-flush-cost-in-the-field","Measuring Vue Flush Cost in the Field",[15,1178,1179,1180,1183,1184,1187,1188,1191,1192,1195],{},"Vue does not expose flush timings to RUM by default, but you can approximate them. Wrap expensive actions with ",[224,1181,1182],{},"performance.mark()"," before the state change and after ",[224,1185,1186],{},"await nextTick()","; the measure between them covers the reactive flush and DOM patch. Send it with your INP beacon for the matching interaction target. Over a week this tells you which components' updates dominate processing time on real devices, which is far more reliable than guessing from a desktop profile. Combine it with the ",[224,1189,1190],{},"web-vitals"," attribution build's ",[224,1193,1194],{},"processingDuration"," to confirm the flush is the part of the interaction that matters.",[202,1197,1199],{"id":1198},"faq","FAQ",[1201,1202,1205,1209],"details",{"className":1203},[1204],"faq-item",[1206,1207,1208],"summary",{},"Does Vue batch multiple state changes in one handler?",[15,1210,1211],{},"Yes. All changes made synchronously in a handler are flushed together after it returns, so ten assignments cost one render pass, not ten. That is good for total work but means the whole cost lands in one task. Yielding between groups of changes deliberately splits that pass into several.",[1201,1213,1215,1218],{"className":1214},[1204],[1206,1216,1217],{},"Is the Vapor mode or a compiler change going to fix this?",[15,1219,1220],{},"Compiler and runtime improvements reduce per-component overhead, which lowers render cost. They cannot make filtering 5,000 records free or decide which work is urgent. Narrowing reactivity, rendering less and yielding remain necessary for large data regardless of rendering mode.",[1201,1222,1224,1227],{"className":1223},[1204],[1206,1225,1226],{},"Should I move this work to a Web Worker instead?",[15,1228,1229],{},"For pure data processing — filtering, sorting, aggregation — yes, a worker removes it from the main thread entirely, and only the result is assigned to reactive state. Rendering cannot move to a worker, so steps 2 and 4 still apply.",[202,1231,1233],{"id":1232},"related","Related",[207,1235,1236,1243,1250],{},[210,1237,1238,1242],{},[19,1239,1241],{"href":1240},"\u002Fframework-performance\u002Fvue-and-nuxt-performance\u002F","Vue & Nuxt performance"," — the framework-level patterns behind these fixes.",[210,1244,1245,1249],{},[19,1246,1248],{"href":1247},"\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fusing-scheduler-posttask-priorities\u002F","Using scheduler.postTask() priorities"," — ordering the follow-up work.",[210,1251,1252,1256],{},[19,1253,1255],{"href":1254},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Freducing-presentation-delay-from-large-dom-updates\u002F","Reducing presentation delay from large DOM updates"," — the rendering cost after the flush.",[1258,1259,1261],"script",{"type":1260},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Break Up Long Tasks in Vue Event Handlers\",\n  \"description\": \"Why Vue interactions turn into long tasks — reactive fan-out, watcher flushes, large re-renders — and how to split them without breaking reactivity.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Store large datasets with shallowRef\",\n      \"text\": \"Large arrays of records that you replace rather than mutate in place should not be deeply reactive.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Yield before the expensive state change\",\n      \"text\": \"Let the browser paint the pressed state and handle pending input before triggering the heavy flush.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Chunk large recomputations\",\n      \"text\": \"When the derived data is genuinely expensive, compute it in chunks outside the reactive system and assign the result once.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Reduce what re-renders\",\n      \"text\": \"Use stable keys, v-memo for rows whose inputs are unchanged, and render only visible rows for long lists.\"\n    }\n  ]\n}\n",[1258,1263,1264],{"type":1260},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Break Up Long Tasks in Vue Event Handlers\",\n  \"description\": \"Why Vue interactions turn into long tasks — reactive fan-out, watcher flushes, large re-renders — and how to split them without breaking reactivity.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-vue-event-handlers\u002F\"\n  }\n}\n",[1258,1266,1267],{"type":1260},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Core Web Vitals & Measurement\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Optimizing INP with scheduler.yield()\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Breaking Up Long Tasks in Vue Event Handlers\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-vue-event-handlers\u002F\"\n    }\n  ]\n}\n",[1269,1270,1271],"style",{},"html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}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 .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--shiki-light:#024C1A}",{"title":399,"searchDepth":426,"depth":426,"links":1273},[1274,1275,1276,1282,1283,1284,1285,1286],{"id":204,"depth":426,"text":205},{"id":265,"depth":426,"text":266},{"id":383,"depth":426,"text":384,"children":1277},[1278,1279,1280,1281],{"id":388,"depth":450,"text":389},{"id":537,"depth":450,"text":538},{"id":716,"depth":450,"text":717},{"id":912,"depth":450,"text":913},{"id":1104,"depth":426,"text":1105},{"id":1117,"depth":426,"text":1118},{"id":1175,"depth":426,"text":1176},{"id":1198,"depth":426,"text":1199},{"id":1232,"depth":426,"text":1233},"Why Vue interactions turn into long tasks — reactive fan-out, watcher flushes, large re-renders — and how to split them without breaking reactivity.","md",{"slug":1290,"type":1291,"breadcrumb":1292,"datePublished":1300,"dateModified":1300},"breaking-up-long-tasks-in-vue-event-handlers","article",[1293,1296,1297,1298],{"name":1294,"url":1295},"Home","\u002F",{"name":32,"url":31},{"name":27,"url":26},{"name":5,"url":1299},"\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-vue-event-handlers\u002F","2026-10-06","\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-vue-event-handlers",{"title":1303,"description":1304},"Breaking Up Long Tasks in Vue Event Handlers for INP","Vue handlers that mutate large reactive state trigger long watcher flushes and renders. Yield, batch and narrow reactivity to keep Vue interactions under 200ms INP.","core-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-vue-event-handlers\u002Findex","cR5iuhPdCmefJIBU3Q2CT13x6zumgIJvZKidYgT-hhM",[1308,1312],{"title":1309,"path":1310,"stem":1311},"Breaking Up Long Tasks in React Event Handlers","\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers","core-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002Findex",{"title":1313,"path":1314,"stem":1315},"Polyfilling scheduler.yield() Across Browsers","\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fpolyfilling-scheduler-yield-across-browsers","core-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fpolyfilling-scheduler-yield-across-browsers\u002Findex",1791308078619]