[{"data":1,"prerenderedAt":1407},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002F":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002F":1399},{"id":4,"title":5,"body":6,"description":1378,"extension":1379,"meta":1380,"navigation":342,"path":1393,"seo":1394,"stem":1397,"__hash__":1398},"content\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002Findex.md","Breaking Up Long Tasks in React Event Handlers",{"type":7,"value":8,"toc":1366},"minimark",[9,14,42,47,50,92,110,127,242,246,266,275,288,294,298,303,306,643,646,650,656,914,921,925,940,1060,1067,1071,1074,1219,1229,1233,1242,1313,1320,1324,1351,1356,1359,1362],[10,11,13],"h1",{"id":12},"a-react-onchange-handler-freezes-the-ui-and-spikes-inp-to-480ms","A React onChange Handler Freezes the UI and Spikes INP to 480ms",[15,16,17,18,22,23,26,27,30,31,36,37,41],"p",{},"You have a filter input or a \"select all\" checkbox whose ",[19,20,21],"code",{},"onChange"," does real work — filtering ten thousand rows, recomputing totals, updating several pieces of state — and field INP for that interaction sits around ",[19,24,25],{},"480ms",", far past the ",[19,28,29],{},"200ms"," \"good\" boundary. The page visibly stalls for a beat on every keystroke. This page resolves that specific scenario; the scheduling concepts behind it live in ",[32,33,35],"a",{"href":34},"\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002F","optimizing INP with scheduler.yield()",", and the broader interactivity metric is covered in ",[32,38,40],{"href":39},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",".",[43,44,46],"h2",{"id":45},"rapid-diagnosis-in-the-performance-panel","Rapid Diagnosis in the Performance Panel",[15,48,49],{},"Confirm it is a processing-duration problem before changing code:",[51,52,53,61,69,81],"ol",{},[54,55,56,57,60],"li",{},"Open DevTools Performance, enable ",[19,58,59],{},"4x"," CPU throttling, record, and perform the interaction once.",[54,62,63,64,68],{},"Find the interaction in the ",[65,66,67],"strong",{},"Interactions"," track — its bar shows total INP. Click it.",[54,70,71,72,75,76,80],{},"Read the breakdown: a small input delay plus a wide ",[65,73,74],{},"processing"," block means your handler is the long task. A wide block ",[77,78,79],"em",{},"before"," the handler means an upstream task inflated input delay instead.",[54,82,83,84,87,88,91],{},"In the flame chart, the long task will show your handler at the top with React's ",[19,85,86],{},"commitRoot","\u002Frender work beneath it. Any single task wider than ",[19,89,90],{},"50ms"," is the offender.",[15,93,94,95,98,99,102,103,105,106,41],{},"A faster sanity check before the full profile: add a ",[19,96,97],{},"PerformanceObserver"," for ",[19,100,101],{},"longtask"," entries and log their duration during the interaction. If you see a single entry near ",[19,104,25],{}," attributed to your script, the handler is doing too much synchronously and the fix is structural, not micro-optimization. For replaying and ranking interactions across a session, use the workflow in ",[32,107,109],{"href":108},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F","profiling event handlers for INP",[15,111,112,113,116,117,119,120,123,124,126],{},"The distinction that drives the fix is ",[77,114,115],{},"which phase"," is wide. A handler that genuinely runs for ",[19,118,25],{}," is a processing-duration problem and is solved by yielding, deferring, or offloading. A handler that itself runs in ",[19,121,122],{},"40ms"," but reports ",[19,125,25],{}," INP is an input-delay problem — something else was already monopolizing the main thread when the click landed — and the fix belongs in that upstream task, not here. Always read the phase split before touching the handler.",[15,128,129],{},[130,131,138,139,138,143,138,147,138,157,138,164,138,169,138,177,138,181,138,185,138,191,138,195,138,199,138,203,138,207,138,213,138,217,138,223,138,228,138,232,138,235,138,238,138],"svg",{"xmlns":132,"viewBox":133,"width":134,"role":135,"ariaLabel":136,"style":137},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 280","100%","img","A React onChange handler running setState, derivation, and re-render synchronously as one 480ms task, versus the same work deferred with useTransition","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:#001d3d"," ",[140,141,142],"title",{},"React handler blocking vs. deferred work",[144,145,146],"desc",{},"Before: setState, derive, and re-render run as one synchronous 480ms task. After: the input commits immediately and heavy work runs as an interruptible transition.",[148,149],"rect",{"x":150,"y":150,"width":151,"height":152,"rx":153,"fill":154,"stroke":155,"style":156},"1","758","278","10","none","currentColor","stroke-opacity:0.18",[158,159,163],"text",{"x":160,"y":161,"fill":155,"style":162},"24","38","font-size:18px;font-weight:700","onChange: synchronous vs. deferred (budget 50ms)",[158,165,168],{"x":160,"y":166,"fill":155,"style":167},"72","font-size:14px;font-weight:600","Before → INP 480ms",[148,170],{"x":160,"y":171,"width":172,"height":161,"rx":173,"fill":174,"stroke":175,"style":176},"84","150","5","#ffc300","#b8860b","fill-opacity:0.22",[148,178],{"x":179,"y":171,"width":180,"height":161,"rx":173,"fill":174,"stroke":175,"style":176},"178","230",[148,182],{"x":183,"y":171,"width":184,"height":161,"rx":173,"fill":174,"stroke":175,"style":176},"412","290",[158,186,190],{"x":187,"y":188,"fill":155,"style":189},"99","108","font-size:14px;text-anchor:middle","setState",[158,192,194],{"x":193,"y":188,"fill":155,"style":189},"293","derive 10k rows",[158,196,198],{"x":197,"y":188,"fill":155,"style":189},"557","re-render list",[158,200,202],{"x":160,"y":172,"fill":155,"style":201},"font-size:14px","One task blocks the next paint for 480ms.",[158,204,206],{"x":160,"y":205,"fill":155,"style":167},"186","After → INP \u003C 200ms",[148,208],{"x":160,"y":209,"width":210,"height":161,"rx":173,"fill":211,"stroke":211,"style":212},"198","120","#0466c8","fill-opacity:0.14",[158,214,216],{"x":171,"y":215,"fill":155,"style":189},"222","input commits",[218,219],"line",{"x1":172,"y1":220,"x2":172,"y2":221,"stroke":175,"style":222},"192","242","stroke-width:2",[158,224,227],{"x":225,"y":226,"fill":155,"style":167},"158","216","paint → then interruptible transition",[148,229],{"x":230,"y":209,"width":231,"height":161,"rx":173,"fill":211,"stroke":211,"style":212},"430","80",[148,233],{"x":234,"y":209,"width":231,"height":161,"rx":173,"fill":211,"stroke":211,"style":212},"518",[148,236],{"x":237,"y":209,"width":231,"height":161,"rx":173,"fill":211,"stroke":211,"style":212},"606",[158,239,241],{"x":160,"y":240,"fill":155,"style":201},"266","The keystroke is painted first; heavy derivation yields and can be interrupted.",[43,243,245],{"id":244},"root-cause-analysis","Root Cause Analysis",[15,247,248,251,252,254,255,257,258,261,262,265],{},[65,249,250],{},"1. Synchronous setState cascades."," Calling several ",[19,253,190],{},"s that each trigger dependent effects, which set more state, chains multiple render passes into one task. React batches updates inside a handler, but cascading effect-driven updates re-run render and layout repeatedly within the same interaction. The telltale sign in the flame chart is several ",[19,256,86],{}," blocks back to back inside one interaction rather than a single commit. Each pass also forces layout if any effect reads geometry (",[19,259,260],{},"getBoundingClientRect",", ",[19,263,264],{},"offsetHeight","), compounding the cost.",[15,267,268,271,272,274],{},[65,269,270],{},"2. Large list re-render."," A single state change re-renders thousands of list children with no virtualization or memoization, so React's reconciliation and the browser's layout\u002Fpaint dominate the processing phase. The cost scales with the number of mounted DOM nodes, not the number that changed, so even a one-character query that filters the list still pays to reconcile every row. This is the most common cause of the ",[19,273,25],{}," scenario in data-heavy tables.",[15,276,277,280,281,283,284,287],{},[65,278,279],{},"3. Heavy derivation inside the handler."," Filtering, sorting, or aggregating a large array directly in ",[19,282,21],{}," runs that O(n) or O(n log n) work synchronously on every keystroke before React even commits. Because it runs ",[77,285,286],{},"inside"," the handler, it inflates processing duration directly, and because it runs on every keystroke it cannot be amortized — typing \"report\" pays the cost six times.",[15,289,290,293],{},[65,291,292],{},"4. Third-party synchronous work."," An analytics call, a validation library, or a feature-flag SDK invoked inline in the handler runs its own long synchronous block, adding to processing duration on every interaction. These are easy to miss because they look like a single innocuous function call; in the flame chart they appear as a wide block under a node-module path you did not write.",[43,295,297],{"id":296},"step-by-step-resolution","Step-by-Step Resolution",[299,300,302],"h3",{"id":301},"fix-1-keep-the-input-responsive-with-usetransition","Fix 1 — Keep the input responsive with useTransition",[15,304,305],{},"Mark the expensive state update as non-urgent so the keystroke commits and paints immediately while the heavy re-render runs in an interruptible transition. This is the highest-leverage change for the large-list case.",[307,308,313],"pre",{"className":309,"code":310,"language":311,"meta":312,"style":312},"language-jsx shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","import { useState, useTransition, useMemo } from 'react';\n\nfunction Filter({ rows }) {\n  const [query, setQuery] = useState('');\n  const [deferredQuery, setDeferredQuery] = useState('');\n  const [isPending, startTransition] = useTransition();\n\n  function onChange(e) {\n    setQuery(e.target.value);                 \u002F\u002F urgent: input paints now\n    startTransition(() => setDeferredQuery(e.target.value)); \u002F\u002F non-urgent: heavy re-render\n  }\n\n  const visible = useMemo(\n    () => rows.filter((r) => r.name.includes(deferredQuery)),\n    [rows, deferredQuery]\n  );\n  \u002F\u002F trade-off: useTransition shows stale results until the transition lands, so for a\n  \u002F\u002F field that MUST reflect the latest value instantly (e.g. a password meter) it feels\n  \u002F\u002F laggy — use it only when a brief stale paint is acceptable.\n  return \u003CList items={visible} dim={isPending} \u002F>;\n}\n","jsx","",[19,314,315,337,344,364,400,427,452,457,474,487,508,514,519,536,570,576,582,588,594,600,637],{"__ignoreMap":312},[316,317,319,323,327,330,334],"span",{"class":218,"line":318},1,[316,320,322],{"class":321},"sP5qI","import",[316,324,326],{"class":325},"syybb"," { useState, useTransition, useMemo } ",[316,328,329],{"class":321},"from",[316,331,333],{"class":332},"s-_DF"," 'react'",[316,335,336],{"class":325},";\n",[316,338,340],{"class":218,"line":339},2,[316,341,343],{"emptyLinePlaceholder":342},true,"\n",[316,345,347,350,354,357,361],{"class":218,"line":346},3,[316,348,349],{"class":321},"function",[316,351,353],{"class":352},"ssM3C"," Filter",[316,355,356],{"class":325},"({ ",[316,358,360],{"class":359},"seIZK","rows",[316,362,363],{"class":325}," }) {\n",[316,365,367,370,373,377,379,382,385,388,391,394,397],{"class":218,"line":366},4,[316,368,369],{"class":321},"  const",[316,371,372],{"class":325}," [",[316,374,376],{"class":375},"sf6mN","query",[316,378,261],{"class":325},[316,380,381],{"class":375},"setQuery",[316,383,384],{"class":325},"] ",[316,386,387],{"class":321},"=",[316,389,390],{"class":352}," useState",[316,392,393],{"class":325},"(",[316,395,396],{"class":332},"''",[316,398,399],{"class":325},");\n",[316,401,403,405,407,410,412,415,417,419,421,423,425],{"class":218,"line":402},5,[316,404,369],{"class":321},[316,406,372],{"class":325},[316,408,409],{"class":375},"deferredQuery",[316,411,261],{"class":325},[316,413,414],{"class":375},"setDeferredQuery",[316,416,384],{"class":325},[316,418,387],{"class":321},[316,420,390],{"class":352},[316,422,393],{"class":325},[316,424,396],{"class":332},[316,426,399],{"class":325},[316,428,430,432,434,437,439,442,444,446,449],{"class":218,"line":429},6,[316,431,369],{"class":321},[316,433,372],{"class":325},[316,435,436],{"class":375},"isPending",[316,438,261],{"class":325},[316,440,441],{"class":375},"startTransition",[316,443,384],{"class":325},[316,445,387],{"class":321},[316,447,448],{"class":352}," useTransition",[316,450,451],{"class":325},"();\n",[316,453,455],{"class":218,"line":454},7,[316,456,343],{"emptyLinePlaceholder":342},[316,458,460,463,466,468,471],{"class":218,"line":459},8,[316,461,462],{"class":321},"  function",[316,464,465],{"class":352}," onChange",[316,467,393],{"class":325},[316,469,470],{"class":359},"e",[316,472,473],{"class":325},") {\n",[316,475,477,480,483],{"class":218,"line":476},9,[316,478,479],{"class":352},"    setQuery",[316,481,482],{"class":325},"(e.target.value);                 ",[316,484,486],{"class":485},"sIIH1","\u002F\u002F urgent: input paints now\n",[316,488,490,493,496,499,502,505],{"class":218,"line":489},10,[316,491,492],{"class":352},"    startTransition",[316,494,495],{"class":325},"(() ",[316,497,498],{"class":321},"=>",[316,500,501],{"class":352}," setDeferredQuery",[316,503,504],{"class":325},"(e.target.value)); ",[316,506,507],{"class":485},"\u002F\u002F non-urgent: heavy re-render\n",[316,509,511],{"class":218,"line":510},11,[316,512,513],{"class":325},"  }\n",[316,515,517],{"class":218,"line":516},12,[316,518,343],{"emptyLinePlaceholder":342},[316,520,522,524,527,530,533],{"class":218,"line":521},13,[316,523,369],{"class":321},[316,525,526],{"class":375}," visible",[316,528,529],{"class":321}," =",[316,531,532],{"class":352}," useMemo",[316,534,535],{"class":325},"(\n",[316,537,539,542,544,547,550,553,556,559,561,564,567],{"class":218,"line":538},14,[316,540,541],{"class":325},"    () ",[316,543,498],{"class":321},[316,545,546],{"class":325}," rows.",[316,548,549],{"class":352},"filter",[316,551,552],{"class":325},"((",[316,554,555],{"class":359},"r",[316,557,558],{"class":325},") ",[316,560,498],{"class":321},[316,562,563],{"class":325}," r.name.",[316,565,566],{"class":352},"includes",[316,568,569],{"class":325},"(deferredQuery)),\n",[316,571,573],{"class":218,"line":572},15,[316,574,575],{"class":325},"    [rows, deferredQuery]\n",[316,577,579],{"class":218,"line":578},16,[316,580,581],{"class":325},"  );\n",[316,583,585],{"class":218,"line":584},17,[316,586,587],{"class":485},"  \u002F\u002F trade-off: useTransition shows stale results until the transition lands, so for a\n",[316,589,591],{"class":218,"line":590},18,[316,592,593],{"class":485},"  \u002F\u002F field that MUST reflect the latest value instantly (e.g. a password meter) it feels\n",[316,595,597],{"class":218,"line":596},19,[316,598,599],{"class":485},"  \u002F\u002F laggy — use it only when a brief stale paint is acceptable.\n",[316,601,603,606,609,613,616,619,622,625,628,630,632,634],{"class":218,"line":602},20,[316,604,605],{"class":321},"  return",[316,607,608],{"class":325}," \u003C",[316,610,612],{"class":611},"s-fAs","List",[316,614,615],{"class":375}," items",[316,617,618],{"class":321},"={",[316,620,621],{"class":325},"visible",[316,623,624],{"class":321},"}",[316,626,627],{"class":375}," dim",[316,629,618],{"class":321},[316,631,436],{"class":325},[316,633,624],{"class":321},[316,635,636],{"class":325}," \u002F>;\n",[316,638,640],{"class":218,"line":639},21,[316,641,642],{"class":325},"}\n",[15,644,645],{},"Expected outcome: input delay drops to near zero and the keystroke paints in one frame, moving processing out of the interaction's critical path — typically cuts INP from ~480ms to under 200ms on the large-list case.",[299,647,649],{"id":648},"fix-2-yield-between-batches-in-the-derivation","Fix 2 — Yield between batches in the derivation",[15,651,652,653,655],{},"When the heavy work is an imperative loop rather than a render (building an index, transforming rows), split it with the await-yield pattern so no chunk exceeds the ",[19,654,90],{}," budget.",[307,657,659],{"className":309,"code":658,"language":311,"meta":312,"style":312},"async function buildIndex(rows, signal) {\n  const index = new Map();\n  let deadline = performance.now() + 50; \u002F\u002F 50ms long-task budget\n  for (let i = 0; i \u003C rows.length; i++) {\n    index.set(rows[i].id, normalize(rows[i]));\n    if (performance.now() >= deadline) {\n      await (window.scheduler?.yield?.() ?? new Promise((r) => setTimeout(r, 0)));\n      if (signal.aborted) return null; \u002F\u002F a newer keystroke superseded this run\n      deadline = performance.now() + 50;\n    }\n  }\n  return index;\n  \u002F\u002F trade-off: yielding makes the index arrive a few frames later, so reads must\n  \u002F\u002F tolerate a transient null\u002Fpartial result — guard consumers accordingly.\n}\n",[19,660,661,683,700,731,769,786,804,846,865,884,889,893,900,905,910],{"__ignoreMap":312},[316,662,663,666,669,672,674,676,678,681],{"class":218,"line":318},[316,664,665],{"class":321},"async",[316,667,668],{"class":321}," function",[316,670,671],{"class":352}," buildIndex",[316,673,393],{"class":325},[316,675,360],{"class":359},[316,677,261],{"class":325},[316,679,680],{"class":359},"signal",[316,682,473],{"class":325},[316,684,685,687,690,692,695,698],{"class":218,"line":339},[316,686,369],{"class":321},[316,688,689],{"class":375}," index",[316,691,529],{"class":321},[316,693,694],{"class":321}," new",[316,696,697],{"class":352}," Map",[316,699,451],{"class":325},[316,701,702,705,708,710,713,716,719,722,725,728],{"class":218,"line":346},[316,703,704],{"class":321},"  let",[316,706,707],{"class":325}," deadline ",[316,709,387],{"class":321},[316,711,712],{"class":325}," performance.",[316,714,715],{"class":352},"now",[316,717,718],{"class":325},"() ",[316,720,721],{"class":321},"+",[316,723,724],{"class":375}," 50",[316,726,727],{"class":325},"; ",[316,729,730],{"class":485},"\u002F\u002F 50ms long-task budget\n",[316,732,733,736,739,742,745,747,750,753,756,758,761,764,767],{"class":218,"line":366},[316,734,735],{"class":321},"  for",[316,737,738],{"class":325}," (",[316,740,741],{"class":321},"let",[316,743,744],{"class":325}," i ",[316,746,387],{"class":321},[316,748,749],{"class":375}," 0",[316,751,752],{"class":325},"; i ",[316,754,755],{"class":321},"\u003C",[316,757,546],{"class":325},[316,759,760],{"class":375},"length",[316,762,763],{"class":325},"; i",[316,765,766],{"class":321},"++",[316,768,473],{"class":325},[316,770,771,774,777,780,783],{"class":218,"line":402},[316,772,773],{"class":325},"    index.",[316,775,776],{"class":352},"set",[316,778,779],{"class":325},"(rows[i].id, ",[316,781,782],{"class":352},"normalize",[316,784,785],{"class":325},"(rows[i]));\n",[316,787,788,791,794,796,798,801],{"class":218,"line":429},[316,789,790],{"class":321},"    if",[316,792,793],{"class":325}," (performance.",[316,795,715],{"class":352},[316,797,718],{"class":325},[316,799,800],{"class":321},">=",[316,802,803],{"class":325}," deadline) {\n",[316,805,806,809,812,815,818,821,823,826,828,830,832,834,837,840,843],{"class":218,"line":454},[316,807,808],{"class":321},"      await",[316,810,811],{"class":325}," (window.scheduler?.",[316,813,814],{"class":352},"yield",[316,816,817],{"class":325},"?.() ",[316,819,820],{"class":321},"??",[316,822,694],{"class":321},[316,824,825],{"class":375}," Promise",[316,827,552],{"class":325},[316,829,555],{"class":359},[316,831,558],{"class":325},[316,833,498],{"class":321},[316,835,836],{"class":352}," setTimeout",[316,838,839],{"class":325},"(r, ",[316,841,842],{"class":375},"0",[316,844,845],{"class":325},")));\n",[316,847,848,851,854,857,860,862],{"class":218,"line":459},[316,849,850],{"class":321},"      if",[316,852,853],{"class":325}," (signal.aborted) ",[316,855,856],{"class":321},"return",[316,858,859],{"class":375}," null",[316,861,727],{"class":325},[316,863,864],{"class":485},"\u002F\u002F a newer keystroke superseded this run\n",[316,866,867,870,872,874,876,878,880,882],{"class":218,"line":476},[316,868,869],{"class":325},"      deadline ",[316,871,387],{"class":321},[316,873,712],{"class":325},[316,875,715],{"class":352},[316,877,718],{"class":325},[316,879,721],{"class":321},[316,881,724],{"class":375},[316,883,336],{"class":325},[316,885,886],{"class":218,"line":489},[316,887,888],{"class":325},"    }\n",[316,890,891],{"class":218,"line":510},[316,892,513],{"class":325},[316,894,895,897],{"class":218,"line":516},[316,896,605],{"class":321},[316,898,899],{"class":325}," index;\n",[316,901,902],{"class":218,"line":521},[316,903,904],{"class":485},"  \u002F\u002F trade-off: yielding makes the index arrive a few frames later, so reads must\n",[316,906,907],{"class":218,"line":538},[316,908,909],{"class":485},"  \u002F\u002F tolerate a transient null\u002Fpartial result — guard consumers accordingly.\n",[316,911,912],{"class":218,"line":572},[316,913,642],{"class":325},[15,915,916,917,920],{},"Expected outcome: a single ",[19,918,919],{},"220ms"," derivation becomes five ~44ms chunks; queued input runs between them, removing the long task and reducing the processing phase by ~200ms.",[299,922,924],{"id":923},"fix-3-defer-non-urgent-third-party-work-out-of-the-handler","Fix 3 — Defer non-urgent third-party work out of the handler",[15,926,927,928,931,932,935,936,939],{},"Move analytics, logging, and flag evaluation off the interaction's critical path with ",[19,929,930],{},"scheduler.postTask"," at ",[19,933,934],{},"background"," priority (or ",[19,937,938],{},"requestIdleCallback","), so they no longer add to processing duration.",[307,941,943],{"className":309,"code":942,"language":311,"meta":312,"style":312},"function onClick() {\n  applyFilter();                              \u002F\u002F urgent, stays in the handler\n  const send = () => analytics.track('filter_applied', { query });\n  if (window.scheduler?.postTask) {\n    window.scheduler.postTask(send, { priority: 'background' });\n  } else {\n    setTimeout(send, 0);\n  }\n  \u002F\u002F trade-off: background-scheduled analytics can be dropped if the user navigates away\n  \u002F\u002F before idle — for must-deliver events use navigator.sendBeacon synchronously instead.\n}\n",[19,944,945,955,966,994,1002,1019,1030,1042,1046,1051,1056],{"__ignoreMap":312},[316,946,947,949,952],{"class":218,"line":318},[316,948,349],{"class":321},[316,950,951],{"class":352}," onClick",[316,953,954],{"class":325},"() {\n",[316,956,957,960,963],{"class":218,"line":339},[316,958,959],{"class":352},"  applyFilter",[316,961,962],{"class":325},"();                              ",[316,964,965],{"class":485},"\u002F\u002F urgent, stays in the handler\n",[316,967,968,970,973,975,978,980,983,986,988,991],{"class":218,"line":346},[316,969,369],{"class":321},[316,971,972],{"class":352}," send",[316,974,529],{"class":321},[316,976,977],{"class":325}," () ",[316,979,498],{"class":321},[316,981,982],{"class":325}," analytics.",[316,984,985],{"class":352},"track",[316,987,393],{"class":325},[316,989,990],{"class":332},"'filter_applied'",[316,992,993],{"class":325},", { query });\n",[316,995,996,999],{"class":218,"line":366},[316,997,998],{"class":321},"  if",[316,1000,1001],{"class":325}," (window.scheduler?.postTask) {\n",[316,1003,1004,1007,1010,1013,1016],{"class":218,"line":402},[316,1005,1006],{"class":325},"    window.scheduler.",[316,1008,1009],{"class":352},"postTask",[316,1011,1012],{"class":325},"(send, { priority: ",[316,1014,1015],{"class":332},"'background'",[316,1017,1018],{"class":325}," });\n",[316,1020,1021,1024,1027],{"class":218,"line":429},[316,1022,1023],{"class":325},"  } ",[316,1025,1026],{"class":321},"else",[316,1028,1029],{"class":325}," {\n",[316,1031,1032,1035,1038,1040],{"class":218,"line":454},[316,1033,1034],{"class":352},"    setTimeout",[316,1036,1037],{"class":325},"(send, ",[316,1039,842],{"class":375},[316,1041,399],{"class":325},[316,1043,1044],{"class":218,"line":459},[316,1045,513],{"class":325},[316,1047,1048],{"class":218,"line":476},[316,1049,1050],{"class":485},"  \u002F\u002F trade-off: background-scheduled analytics can be dropped if the user navigates away\n",[316,1052,1053],{"class":218,"line":489},[316,1054,1055],{"class":485},"  \u002F\u002F before idle — for must-deliver events use navigator.sendBeacon synchronously instead.\n",[316,1057,1058],{"class":218,"line":510},[316,1059,642],{"class":325},[15,1061,1062,1063,1066],{},"Expected outcome: removes the third-party synchronous block from the interaction, trimming processing duration by however long that SDK ran (commonly ",[19,1064,1065],{},"30–80ms",").",[299,1068,1070],{"id":1069},"fix-4-move-cpu-bound-work-to-a-worker","Fix 4 — Move CPU-bound work to a worker",[15,1072,1073],{},"If the derivation is genuinely heavy (parsing, large-array math, fuzzy search), yielding only makes it interruptible — it does not make it cheap. Offload it entirely.",[307,1075,1077],{"className":309,"code":1076,"language":311,"meta":312,"style":312},"import { wrap } from 'comlink';\nconst search = wrap(new Worker(new URL('.\u002Fsearch.worker.js', import.meta.url), { type: 'module' }));\n\nasync function onChange(e) {\n  const results = await search.filter(e.target.value); \u002F\u002F runs off the main thread\n  startTransition(() => setResults(results));\n  \u002F\u002F trade-off: the worker round-trip adds postMessage serialization latency, so for\n  \u002F\u002F small\u002Fcheap derivations a main-thread useMemo is faster — reserve workers for jobs\n  \u002F\u002F that exceed ~50ms of pure computation.\n}\n",[19,1078,1079,1093,1144,1148,1162,1185,1200,1205,1210,1215],{"__ignoreMap":312},[316,1080,1081,1083,1086,1088,1091],{"class":218,"line":318},[316,1082,322],{"class":321},[316,1084,1085],{"class":325}," { wrap } ",[316,1087,329],{"class":321},[316,1089,1090],{"class":332}," 'comlink'",[316,1092,336],{"class":325},[316,1094,1095,1098,1101,1103,1106,1108,1111,1114,1116,1118,1121,1123,1126,1128,1130,1132,1135,1138,1141],{"class":218,"line":339},[316,1096,1097],{"class":321},"const",[316,1099,1100],{"class":375}," search",[316,1102,529],{"class":321},[316,1104,1105],{"class":352}," wrap",[316,1107,393],{"class":325},[316,1109,1110],{"class":321},"new",[316,1112,1113],{"class":352}," Worker",[316,1115,393],{"class":325},[316,1117,1110],{"class":321},[316,1119,1120],{"class":352}," URL",[316,1122,393],{"class":325},[316,1124,1125],{"class":332},"'.\u002Fsearch.worker.js'",[316,1127,261],{"class":325},[316,1129,322],{"class":321},[316,1131,41],{"class":325},[316,1133,1134],{"class":375},"meta",[316,1136,1137],{"class":325},".url), { type: ",[316,1139,1140],{"class":332},"'module'",[316,1142,1143],{"class":325}," }));\n",[316,1145,1146],{"class":218,"line":346},[316,1147,343],{"emptyLinePlaceholder":342},[316,1149,1150,1152,1154,1156,1158,1160],{"class":218,"line":366},[316,1151,665],{"class":321},[316,1153,668],{"class":321},[316,1155,465],{"class":352},[316,1157,393],{"class":325},[316,1159,470],{"class":359},[316,1161,473],{"class":325},[316,1163,1164,1166,1169,1171,1174,1177,1179,1182],{"class":218,"line":402},[316,1165,369],{"class":321},[316,1167,1168],{"class":375}," results",[316,1170,529],{"class":321},[316,1172,1173],{"class":321}," await",[316,1175,1176],{"class":325}," search.",[316,1178,549],{"class":352},[316,1180,1181],{"class":325},"(e.target.value); ",[316,1183,1184],{"class":485},"\u002F\u002F runs off the main thread\n",[316,1186,1187,1190,1192,1194,1197],{"class":218,"line":429},[316,1188,1189],{"class":352},"  startTransition",[316,1191,495],{"class":325},[316,1193,498],{"class":321},[316,1195,1196],{"class":352}," setResults",[316,1198,1199],{"class":325},"(results));\n",[316,1201,1202],{"class":218,"line":454},[316,1203,1204],{"class":485},"  \u002F\u002F trade-off: the worker round-trip adds postMessage serialization latency, so for\n",[316,1206,1207],{"class":218,"line":459},[316,1208,1209],{"class":485},"  \u002F\u002F small\u002Fcheap derivations a main-thread useMemo is faster — reserve workers for jobs\n",[316,1211,1212],{"class":218,"line":476},[316,1213,1214],{"class":485},"  \u002F\u002F that exceed ~50ms of pure computation.\n",[316,1216,1217],{"class":218,"line":489},[316,1218,642],{"class":325},[15,1220,1221,1222,1224,1225,41],{},"Expected outcome: the main-thread processing phase collapses to the message round-trip (single-digit ms), holding INP well under ",[19,1223,29],{}," even for expensive payloads. The full setup is in ",[32,1226,1228],{"href":1227},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002F","offloading work to web workers with Comlink",[43,1230,1232],{"id":1231},"verification","Verification",[15,1234,1235,1236,1238,1239,1241],{},"Re-record the interaction under ",[19,1237,59],{}," throttling: the previously wide processing block should now be either a single small commit (Fix 1\u002F4) or a series of sub-",[19,1240,90],{}," chunks (Fix 2), with no task crossing the long-task threshold. In CI, assert Total Blocking Time as the lab proxy:",[307,1243,1247],{"className":1244,"code":1245,"language":1246,"meta":312,"style":312},"language-javascript shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","\u002F\u002F lighthouserc.js — fail the build if main-thread blocking regresses\nmodule.exports = {\n  ci: { assert: { assertions: {\n    'total-blocking-time': ['error', { maxNumericValue: 200 }],\n  } } },\n};\n\u002F\u002F trade-off: TBT is measured at load, so a slow post-load interaction can pass CI while\n\u002F\u002F field INP stays red — confirm the win in RUM at the p75, not just in the lab.\n","javascript",[19,1248,1249,1254,1268,1273,1293,1298,1303,1308],{"__ignoreMap":312},[316,1250,1251],{"class":218,"line":318},[316,1252,1253],{"class":485},"\u002F\u002F lighthouserc.js — fail the build if main-thread blocking regresses\n",[316,1255,1256,1259,1261,1264,1266],{"class":218,"line":339},[316,1257,1258],{"class":375},"module",[316,1260,41],{"class":325},[316,1262,1263],{"class":375},"exports",[316,1265,529],{"class":321},[316,1267,1029],{"class":325},[316,1269,1270],{"class":218,"line":346},[316,1271,1272],{"class":325},"  ci: { assert: { assertions: {\n",[316,1274,1275,1278,1281,1284,1287,1290],{"class":218,"line":366},[316,1276,1277],{"class":332},"    'total-blocking-time'",[316,1279,1280],{"class":325},": [",[316,1282,1283],{"class":332},"'error'",[316,1285,1286],{"class":325},", { maxNumericValue: ",[316,1288,1289],{"class":375},"200",[316,1291,1292],{"class":325}," }],\n",[316,1294,1295],{"class":218,"line":402},[316,1296,1297],{"class":325},"  } } },\n",[316,1299,1300],{"class":218,"line":429},[316,1301,1302],{"class":325},"};\n",[316,1304,1305],{"class":218,"line":454},[316,1306,1307],{"class":485},"\u002F\u002F trade-off: TBT is measured at load, so a slow post-load interaction can pass CI while\n",[316,1309,1310],{"class":218,"line":459},[316,1311,1312],{"class":485},"\u002F\u002F field INP stays red — confirm the win in RUM at the p75, not just in the lab.\n",[15,1314,1315,1316,41],{},"Finally, watch field INP at the 75th percentile for that interaction in your RUM dashboard for a few days; the lab gate prevents regressions merging, and the field number confirms the fix on real devices. For the same techniques applied at app scale, see ",[32,1317,1319],{"href":1318},"\u002Fcore-web-vitals-measurement\u002Foptimizing-first-input-delay-fid\u002Fimproving-inp-for-complex-single-page-applications\u002F","improving INP for complex single page applications",[43,1321,1323],{"id":1322},"related","Related",[1325,1326,1327,1333,1339,1345],"ul",{},[54,1328,1329,1332],{},[32,1330,1331],{"href":34},"Optimizing INP with scheduler.yield()"," — the scheduling APIs and yield-point patterns behind these fixes.",[54,1334,1335,1338],{},[32,1336,1337],{"href":108},"Profiling event handlers for INP"," — locating and ranking the slow interaction in the Performance panel.",[54,1340,1341,1344],{},[32,1342,1343],{"href":1227},"Offloading work to web workers with Comlink"," — moving genuinely heavy derivation off the main thread.",[54,1346,1347,1350],{},[32,1348,1349],{"href":1318},"Improving INP for complex single page applications"," — applying these patterns across router transitions and hydration.",[1352,1353,1355],"script",{"type":1354},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Breaking up long tasks in React event handlers\",\n  \"description\": \"Diagnose and fix a React onClick\u002FonChange handler that blocks the main thread and spikes INP past 200ms.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"name\": \"Keep the input responsive with useTransition\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002F#fix-1-keep-the-input-responsive-with-usetransition\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Yield between batches in the derivation\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002F#fix-2-yield-between-batches-in-the-derivation\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Defer non-urgent third-party work out of the handler\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002F#fix-3-defer-non-urgent-third-party-work-out-of-the-handler\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Move CPU-bound work to a worker\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002F#fix-4-move-cpu-bound-work-to-a-worker\" }\n  ]\n}\n",[1352,1357,1358],{"type":1354},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"A React onChange Handler Freezes the UI and Spikes INP to 480ms\",\n  \"description\": \"Root causes and paste-ready fixes for a React event handler that blocks the main thread and pushes INP past 200ms.\",\n  \"datePublished\": \"2026-06-18\",\n  \"dateModified\": \"2026-06-18\",\n  \"mainEntityOfPage\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002F\"\n}\n",[1352,1360,1361],{"type":1354},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    { \"@type\": \"ListItem\", \"position\": 1, \"name\": \"Home\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 2, \"name\": \"Core Web Vitals & Measurement\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"Optimizing INP with scheduler.yield()\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Breaking up long tasks in React event handlers\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002F\" }\n  ]\n}\n",[1363,1364,1365],"style",{},"html pre.shiki code .sP5qI, html code.shiki .sP5qI{--shiki-default:#A0111F;--shiki-dark:#A0111F;--shiki-light:#A0111F}html pre.shiki code .syybb, html code.shiki .syybb{--shiki-default:#0E1116;--shiki-dark:#0E1116;--shiki-light:#0E1116}html pre.shiki code .s-_DF, html code.shiki .s-_DF{--shiki-default:#032563;--shiki-dark:#032563;--shiki-light:#032563}html pre.shiki code .ssM3C, html code.shiki .ssM3C{--shiki-default:#622CBC;--shiki-dark:#622CBC;--shiki-light:#622CBC}html pre.shiki code .seIZK, html code.shiki .seIZK{--shiki-default:#702C00;--shiki-dark:#702C00;--shiki-light:#702C00}html pre.shiki code .sf6mN, html code.shiki .sf6mN{--shiki-default:#023B95;--shiki-dark:#023B95;--shiki-light:#023B95}html pre.shiki code .sIIH1, html code.shiki .sIIH1{--shiki-default:#66707B;--shiki-dark:#66707B;--shiki-light:#66707B}html pre.shiki code .s-fAs, html code.shiki .s-fAs{--shiki-default:#024C1A;--shiki-dark:#024C1A;--shiki-light:#024C1A}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);}",{"title":312,"searchDepth":339,"depth":339,"links":1367},[1368,1369,1370,1376,1377],{"id":45,"depth":339,"text":46},{"id":244,"depth":339,"text":245},{"id":296,"depth":339,"text":297,"children":1371},[1372,1373,1374,1375],{"id":301,"depth":346,"text":302},{"id":648,"depth":346,"text":649},{"id":923,"depth":346,"text":924},{"id":1069,"depth":346,"text":1070},{"id":1231,"depth":339,"text":1232},{"id":1322,"depth":339,"text":1323},"Diagnose and fix a React event handler that blocks the main thread and pushes INP past 200ms.","md",{"slug":1381,"type":1382,"breadcrumb":1383,"datePublished":1392,"dateModified":1392},"breaking-up-long-tasks-in-react-event-handlers","long_tail",[1384,1387,1388,1389],{"name":1385,"url":1386},"Home","\u002F",{"name":40,"url":39},{"name":1331,"url":34},{"name":1390,"url":1391},"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\u002F","2026-06-18","\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers",{"title":1395,"description":1396},"Breaking Up Long Tasks in React Handlers","Fix a React onClick\u002FonChange handler that blocks the main thread and spikes INP. Root causes and paste-ready fixes with scheduler.yield and useTransition.","core-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Fbreaking-up-long-tasks-in-react-event-handlers\u002Findex","e2Z48FRYm6C3OoGVu2Drk67W1xbFRGdAgssxCiRGK_4",[1400,1403],{"title":1331,"path":1401,"stem":1402,"children":-1},"\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield","core-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002Findex",{"title":1404,"path":1405,"stem":1406,"children":-1},"Profiling Event Handlers for INP","\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp","core-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Findex",1782237171381]