[{"data":1,"prerenderedAt":1236},["ShallowReactive",2],{"content:\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fresizeobserver-vs-resize-and-scroll-handlers":3,"surroundings:\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fresizeobserver-vs-resize-and-scroll-handlers":1230},{"id":4,"title":5,"body":6,"description":1210,"extension":1211,"meta":1212,"navigation":1223,"path":1224,"seo":1225,"stem":1228,"__hash__":1229},"content\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fresizeobserver-vs-resize-and-scroll-handlers\u002Findex.md","ResizeObserver vs Resize and Scroll Handlers",{"type":7,"value":8,"toc":1192},"minimark",[9,13,37,46,190,195,231,235,241,247,253,263,267,272,454,458,577,581,588,592,607,670,674,677,681,687,844,848,851,995,999,1002,1006,1032,1036,1046,1056,1066,1072,1076,1088,1097,1106,1115,1128,1137,1150,1154,1177,1182,1185,1188],[10,11,5],"h1",{"id":12},"resizeobserver-vs-resize-and-scroll-handlers",[14,15,16,17,22,23,27,28,32,33,36],"p",{},"This guide is part of ",[18,19,21],"a",{"href":20},"\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002F","Layout Thrashing & Forced Reflow",", within ",[18,24,26],{"href":25},"\u002Frendering-css-performance\u002F","Rendering & CSS Performance",". For years, responsive behaviour in JavaScript meant listening to ",[29,30,31],"code",{},"window.resize"," and ",[29,34,35],{},"scroll"," events and measuring elements in the handler: is the sidebar narrower than 300px, is this section in view, how tall is the header now. These handlers fire very often — scroll events can fire on every frame, resize events many times per second while dragging — and each measurement may force a layout if anything changed since the last frame. The result is jank during scrolling and resizing and busy main threads that delay input.",[14,38,39,32,42,45],{},[29,40,41],{},"ResizeObserver",[29,43,44],{},"IntersectionObserver"," replace most of these patterns. They deliver exactly the information needed (element sizes, visibility changes) at points in the rendering cycle where layout is already computed, only when something changed, and without per-event polling. They are also element-specific: a component can react to its own size rather than the window's.",[14,47,48],{},[49,50,56,57,56,64,56,68,56,71,56,80,56,86,56,92,56,99,56,103,56,108,56,111,56,115,56,119,56,123,56,128,56,132,56,136,56,139,56,142,56,146,56,148,56,151,56,153,56,156,56,159,56,163,56,165,56,168,56,170,56,173,56,176,56,180,56,182,56,185,56,187,56],"svg",{"viewBox":51,"width":52,"role":53,"ariaLabel":54,"style":55},"0 0 760 228","100%","img","Comparison of resize and scroll event handlers with ResizeObserver and IntersectionObserver.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[58,59],"rect",{"className":60,"x":62,"y":62,"width":52,"height":52,"fill":63},[61],"svg-canvas","0","#ffffff",[65,66,67],"title",{},"Event handlers vs observers",[69,70,54],"desc",{},[58,72],{"x":73,"y":73,"width":74,"height":75,"rx":76,"fill":77,"stroke":78,"style":79},"1","758","226","10","none","currentColor","stroke-opacity:0.18",[81,82,67],"text",{"x":83,"y":84,"fill":78,"style":85},"28.0","34.0","font-size:16px;font-weight:700",[58,87],{"x":83,"y":88,"width":89,"height":90,"rx":62,"fill":78,"stroke":78,"style":91},"56.0","121.9","30.0","fill-opacity:0.06;stroke-opacity:0.4",[81,93,98],{"x":94,"y":95,"fill":78,"style":96,"textAnchor":97},"38.0","75.5","font-size:12.5px;font-weight:700","start","Aspect",[58,100],{"x":101,"y":88,"width":102,"height":90,"rx":62,"fill":78,"stroke":78,"style":91},"149.9","291.1",[81,104,107],{"x":105,"y":95,"fill":78,"style":96,"textAnchor":106},"295.4","middle","resize \u002F scroll handlers",[58,109],{"x":110,"y":88,"width":102,"height":90,"rx":62,"fill":78,"stroke":78,"style":91},"440.9",[81,112,114],{"x":113,"y":95,"fill":78,"style":96,"textAnchor":106},"586.5","Observers",[58,116],{"x":83,"y":117,"width":89,"height":90,"rx":62,"fill":77,"stroke":78,"style":118},"86.0","stroke-opacity:0.35",[81,120,122],{"x":94,"y":121,"fill":78,"style":96,"textAnchor":97},"105.5","When they fire",[58,124],{"x":101,"y":117,"width":102,"height":90,"rx":62,"fill":125,"stroke":126,"style":127},"#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[81,129,131],{"x":105,"y":121,"fill":78,"style":130,"textAnchor":106},"font-size:12px","every event, many per second",[58,133],{"x":110,"y":117,"width":102,"height":90,"rx":62,"fill":134,"stroke":134,"style":135},"#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[81,137,138],{"x":113,"y":121,"fill":78,"style":130,"textAnchor":106},"only on change",[58,140],{"x":83,"y":141,"width":89,"height":90,"rx":62,"fill":77,"stroke":78,"style":118},"116.0",[81,143,145],{"x":94,"y":144,"fill":78,"style":96,"textAnchor":97},"135.5","Layout reads",[58,147],{"x":101,"y":141,"width":102,"height":90,"rx":62,"fill":125,"stroke":126,"style":127},[81,149,150],{"x":105,"y":144,"fill":78,"style":130,"textAnchor":106},"force layout if dirty",[58,152],{"x":110,"y":141,"width":102,"height":90,"rx":62,"fill":134,"stroke":134,"style":135},[81,154,155],{"x":113,"y":144,"fill":78,"style":130,"textAnchor":106},"sizes delivered after layout",[58,157],{"x":83,"y":158,"width":89,"height":90,"rx":62,"fill":77,"stroke":78,"style":118},"146.0",[81,160,162],{"x":94,"y":161,"fill":78,"style":96,"textAnchor":97},"165.5","Granularity",[58,164],{"x":101,"y":158,"width":102,"height":90,"rx":62,"fill":125,"stroke":126,"style":127},[81,166,167],{"x":105,"y":161,"fill":78,"style":130,"textAnchor":106},"window-level",[58,169],{"x":110,"y":158,"width":102,"height":90,"rx":62,"fill":134,"stroke":134,"style":135},[81,171,172],{"x":113,"y":161,"fill":78,"style":130,"textAnchor":106},"per element",[58,174],{"x":83,"y":175,"width":89,"height":90,"rx":62,"fill":77,"stroke":78,"style":118},"176.0",[81,177,179],{"x":94,"y":178,"fill":78,"style":96,"textAnchor":97},"195.5","Throttling needed",[58,181],{"x":101,"y":175,"width":102,"height":90,"rx":62,"fill":125,"stroke":126,"style":127},[81,183,184],{"x":105,"y":178,"fill":78,"style":130,"textAnchor":106},"yes",[58,186],{"x":110,"y":175,"width":102,"height":90,"rx":62,"fill":134,"stroke":134,"style":135},[81,188,189],{"x":113,"y":178,"fill":78,"style":130,"textAnchor":106},"built in",[191,192,194],"h2",{"id":193},"rapid-diagnosis","Rapid Diagnosis",[196,197,198,213,219,225],"ul",{},[199,200,201,205,206,32,209,212],"li",{},[202,203,204],"strong",{},"Search the codebase"," for ",[29,207,208],{},"addEventListener('resize'",[29,210,211],{},"addEventListener('scroll'"," and list what each handler reads and writes.",[199,214,215,218],{},[202,216,217],{},"Record a trace while scrolling"," and resizing; look for handler tasks with Layout events and forced reflow warnings.",[199,220,221,224],{},[202,222,223],{},"Check for throttling and debouncing utilities"," wrapped around handlers — a sign that handlers were known to be expensive.",[199,226,227,230],{},[202,228,229],{},"Check for non-passive touch and wheel listeners",", which can delay scrolling itself.",[191,232,234],{"id":233},"root-cause-analysis","Root Cause Analysis",[14,236,237,240],{},[202,238,239],{},"1. High event frequency."," Handlers run far more often than anything actually changes.",[14,242,243,246],{},[202,244,245],{},"2. Measuring in handlers."," Reads after other writes in the same frame force layout.",[14,248,249,252],{},[202,250,251],{},"3. Window-level signals for element-level questions."," Components care about their own size, not the window's.",[14,254,255,258,259,262],{},[202,256,257],{},"4. Visibility by geometry."," Computing \"is it in view\" with ",[29,260,261],{},"getBoundingClientRect()"," on scroll for many elements.",[191,264,266],{"id":265},"step-by-step-resolution","Step-by-Step Resolution",[268,269,271],"h3",{"id":270},"_1-replace-resize-plus-measure-with-resizeobserver","1. Replace resize-plus-measure with ResizeObserver",[273,274,279],"pre",{"className":275,"code":276,"language":277,"meta":278,"style":278},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","const ro = new ResizeObserver((entries) => {\n  for (const entry of entries) {\n    const width = entry.contentBoxSize[0].inlineSize;     \u002F\u002F delivered, not read\n    entry.target.classList.toggle('compact', width \u003C 420);\n  }\n});\ndocument.querySelectorAll('.card-list').forEach((el) => ro.observe(el));\n\u002F\u002F trade-off: toggling classes that change the observed element's size can cause\n\u002F\u002F a resize loop; the browser stops it with an error, so design for stable sizes.\n","javascript","",[29,280,281,321,341,364,392,398,404,442,448],{"__ignoreMap":278},[282,283,286,290,294,297,300,304,308,312,315,318],"span",{"class":284,"line":285},"line",1,[282,287,289],{"class":288},"sPARh","const",[282,291,293],{"class":292},"sPXB4"," ro",[282,295,296],{"class":288}," =",[282,298,299],{"class":288}," new",[282,301,303],{"class":302},"smZ65"," ResizeObserver",[282,305,307],{"class":306},"saISM","((",[282,309,311],{"class":310},"sQw3B","entries",[282,313,314],{"class":306},") ",[282,316,317],{"class":288},"=>",[282,319,320],{"class":306}," {\n",[282,322,324,327,330,332,335,338],{"class":284,"line":323},2,[282,325,326],{"class":288},"  for",[282,328,329],{"class":306}," (",[282,331,289],{"class":288},[282,333,334],{"class":292}," entry",[282,336,337],{"class":288}," of",[282,339,340],{"class":306}," entries) {\n",[282,342,344,347,350,352,355,357,360],{"class":284,"line":343},3,[282,345,346],{"class":288},"    const",[282,348,349],{"class":292}," width",[282,351,296],{"class":288},[282,353,354],{"class":306}," entry.contentBoxSize[",[282,356,62],{"class":292},[282,358,359],{"class":306},"].inlineSize;     ",[282,361,363],{"class":362},"sjfSM","\u002F\u002F delivered, not read\n",[282,365,367,370,373,376,380,383,386,389],{"class":284,"line":366},4,[282,368,369],{"class":306},"    entry.target.classList.",[282,371,372],{"class":302},"toggle",[282,374,375],{"class":306},"(",[282,377,379],{"class":378},"sZ8jY","'compact'",[282,381,382],{"class":306},", width ",[282,384,385],{"class":288},"\u003C",[282,387,388],{"class":292}," 420",[282,390,391],{"class":306},");\n",[282,393,395],{"class":284,"line":394},5,[282,396,397],{"class":306},"  }\n",[282,399,401],{"class":284,"line":400},6,[282,402,403],{"class":306},"});\n",[282,405,407,410,413,415,418,421,424,426,429,431,433,436,439],{"class":284,"line":406},7,[282,408,409],{"class":306},"document.",[282,411,412],{"class":302},"querySelectorAll",[282,414,375],{"class":306},[282,416,417],{"class":378},"'.card-list'",[282,419,420],{"class":306},").",[282,422,423],{"class":302},"forEach",[282,425,307],{"class":306},[282,427,428],{"class":310},"el",[282,430,314],{"class":306},[282,432,317],{"class":288},[282,434,435],{"class":306}," ro.",[282,437,438],{"class":302},"observe",[282,440,441],{"class":306},"(el));\n",[282,443,445],{"class":284,"line":444},8,[282,446,447],{"class":362},"\u002F\u002F trade-off: toggling classes that change the observed element's size can cause\n",[282,449,451],{"class":284,"line":450},9,[282,452,453],{"class":362},"\u002F\u002F a resize loop; the browser stops it with an error, so design for stable sizes.\n",[268,455,457],{"id":456},"_2-replace-scroll-plus-geometry-with-intersectionobserver","2. Replace scroll-plus-geometry with IntersectionObserver",[273,459,461],{"className":275,"code":460,"language":277,"meta":278,"style":278},"const io = new IntersectionObserver((entries) => {\n  for (const e of entries) e.target.classList.toggle('in-view', e.isIntersecting);\n}, { rootMargin: '0px 0px -20% 0px', threshold: [0, 0.5] });\ndocument.querySelectorAll('section[data-track]').forEach((s) => io.observe(s));\n\u002F\u002F trade-off: callbacks are asynchronous and may lag a frame behind scrolling;\n\u002F\u002F use CSS scroll-driven animations for effects that must track scroll exactly.\n",[29,462,463,487,513,535,567,572],{"__ignoreMap":278},[282,464,465,467,470,472,474,477,479,481,483,485],{"class":284,"line":285},[282,466,289],{"class":288},[282,468,469],{"class":292}," io",[282,471,296],{"class":288},[282,473,299],{"class":288},[282,475,476],{"class":302}," IntersectionObserver",[282,478,307],{"class":306},[282,480,311],{"class":310},[282,482,314],{"class":306},[282,484,317],{"class":288},[282,486,320],{"class":306},[282,488,489,491,493,495,498,500,503,505,507,510],{"class":284,"line":323},[282,490,326],{"class":288},[282,492,329],{"class":306},[282,494,289],{"class":288},[282,496,497],{"class":292}," e",[282,499,337],{"class":288},[282,501,502],{"class":306}," entries) e.target.classList.",[282,504,372],{"class":302},[282,506,375],{"class":306},[282,508,509],{"class":378},"'in-view'",[282,511,512],{"class":306},", e.isIntersecting);\n",[282,514,515,518,521,524,526,529,532],{"class":284,"line":343},[282,516,517],{"class":306},"}, { rootMargin: ",[282,519,520],{"class":378},"'0px 0px -20% 0px'",[282,522,523],{"class":306},", threshold: [",[282,525,62],{"class":292},[282,527,528],{"class":306},", ",[282,530,531],{"class":292},"0.5",[282,533,534],{"class":306},"] });\n",[282,536,537,539,541,543,546,548,550,552,555,557,559,562,564],{"class":284,"line":366},[282,538,409],{"class":306},[282,540,412],{"class":302},[282,542,375],{"class":306},[282,544,545],{"class":378},"'section[data-track]'",[282,547,420],{"class":306},[282,549,423],{"class":302},[282,551,307],{"class":306},[282,553,554],{"class":310},"s",[282,556,314],{"class":306},[282,558,317],{"class":288},[282,560,561],{"class":306}," io.",[282,563,438],{"class":302},[282,565,566],{"class":306},"(s));\n",[282,568,569],{"class":284,"line":394},[282,570,571],{"class":362},"\u002F\u002F trade-off: callbacks are asynchronous and may lag a frame behind scrolling;\n",[282,573,574],{"class":284,"line":400},[282,575,576],{"class":362},"\u002F\u002F use CSS scroll-driven animations for effects that must track scroll exactly.\n",[268,578,580],{"id":579},"_3-use-css-where-possible","3. Use CSS where possible",[14,582,583,584,587],{},"Container queries replace most \"component width\" scripts; ",[29,585,586],{},"position: sticky"," replaces header-fixing scripts; scroll-driven animations replace parallax and progress-bar scroll handlers.",[268,589,591],{"id":590},"_4-keep-any-remaining-scroll-handlers-passive-and-cheap","4. Keep any remaining scroll handlers passive and cheap",[14,593,594,595,598,599,602,603,606],{},"If a scroll handler is unavoidable, register it as ",[29,596,597],{},"{ passive: true }",", read only ",[29,600,601],{},"scrollY"," (cheap when layout is clean), and defer writes to ",[29,604,605],{},"requestAnimationFrame",".",[14,608,609],{},[49,610,56,613,56,616,56,619,56,621,56,624,56,626,56,633,56,640,56,645,56,649,56,654,56,658,56,661,56,666,56],{"viewBox":611,"width":52,"role":53,"ariaLabel":612,"style":55},"0 0 760 163","Bar chart comparing main-thread time spent in scroll-related work for handler-based and observer-based implementations.",[58,614],{"className":615,"x":62,"y":62,"width":52,"height":52,"fill":63},[61],[65,617,618],{},"Main-thread time during a 5-second scroll (article page)",[69,620,612],{},[58,622],{"x":73,"y":73,"width":74,"height":623,"rx":76,"fill":77,"stroke":78,"style":79},"161",[81,625,618],{"x":83,"y":84,"fill":78,"style":85},[81,627,632],{"x":628,"y":629,"fill":78,"style":630,"textAnchor":631},"279.8","70.0","font-size:13px","end","Scroll handler + getBoundingClientRect x 40",[58,634],{"x":635,"y":88,"width":636,"height":637,"rx":638,"fill":77,"stroke":78,"style":639},"291.8","376.2","19","3","fill-opacity:0.7;stroke-opacity:0.35",[81,641,644],{"x":642,"y":629,"fill":78,"style":643},"674.0","font-size:12px;font-weight:600","1450ms",[81,646,648],{"x":628,"y":647,"fill":78,"style":630,"textAnchor":631},"101.0","Throttled handler (100ms)",[58,650],{"x":635,"y":651,"width":652,"height":637,"rx":638,"fill":134,"stroke":134,"style":653},"87.0","109.0","fill-opacity:0.55;stroke-opacity:0.9",[81,655,657],{"x":656,"y":647,"fill":78,"style":643},"406.8","420ms",[81,659,44],{"x":628,"y":660,"fill":78,"style":630,"textAnchor":631},"132.0",[58,662],{"x":635,"y":663,"width":664,"height":637,"rx":638,"fill":125,"stroke":126,"style":665},"118.0","9.1","fill-opacity:0.7;stroke-opacity:0.9",[81,667,669],{"x":668,"y":660,"fill":78,"style":643},"306.9","35ms",[191,671,673],{"id":672},"verification","Verification",[14,675,676],{},"Record traces while scrolling and resizing. Handler tasks should disappear; observer callbacks should be small and infrequent, without forced reflow warnings. Scrolling should show no dropped frames in the Frames track. In the field, compare INP and any smoothness metrics you collect for the affected pages.",[191,678,680],{"id":679},"worked-example-a-documentation-table-of-contents","Worked Example: A Documentation Table of Contents",[14,682,683,684,686],{},"A documentation site highlighted the current section in a sidebar table of contents. Its scroll handler called ",[29,685,261],{}," on all 60 headings on every scroll event and toggled classes, causing forced reflows each time after the first toggle. Scrolling on mid-range phones dropped frames visibly, and the trace showed 25–30ms handler tasks. Replacing the handler with an IntersectionObserver on headings, with a root margin defining an \"active zone\" near the top of the viewport, removed the scroll handler entirely. Scroll tasks disappeared, and scrolling became smooth.",[14,688,689],{},[49,690,56,693,56,696,56,699,56,701,56,718,56,721,56,723,56,728,56,734,56,737,56,742,56,750,56,755,56,759,56,765,56,768,56,772,56,774,56,777,56,780,56,783,56,786,56,789,56,792,56,796,56,798,56,800,56,803,56,806,56,810,56,813,56,816,56,820,56,822,56,824,56,827,56,830,56,834,56,837,56,840,56],{"viewBox":691,"width":52,"role":53,"ariaLabel":692,"style":55},"0 0 760 396","Decision sequence for replacing resize and scroll handlers with CSS or observers.",[58,694],{"className":695,"x":62,"y":62,"width":52,"height":52,"fill":63},[61],[65,697,698],{},"Which tool replaces this handler?",[69,700,692],{},[702,703,704],"defs",{},[705,706,713],"marker",{"id":707,"viewBox":708,"refX":709,"refY":710,"markerWidth":711,"markerHeight":711,"orient":712},"fa5b310978","0 0 10 10","9","5","7","auto-start-reverse",[714,715],"path",{"d":716,"fill":78,"style":717},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[58,719],{"x":73,"y":73,"width":74,"height":720,"rx":76,"fill":77,"stroke":78,"style":79},"394",[81,722,698],{"x":83,"y":84,"fill":78,"style":85},[58,724],{"x":83,"y":88,"width":725,"height":726,"rx":727,"fill":78,"stroke":78,"style":91},"320.0","35.0","6",[81,729,733],{"x":730,"y":731,"fill":78,"style":732,"textAnchor":106},"188.0","78.0","font-size:13px;font-weight:700","Is it only styling based on a container's width?",[58,735],{"x":736,"y":88,"width":725,"height":726,"rx":727,"fill":125,"stroke":126,"style":127},"412.0",[81,738,741],{"x":739,"y":731,"fill":78,"style":740,"textAnchor":106},"572.0","font-size:12.5px","Container queries",[284,743],{"x1":744,"y1":745,"x2":746,"y2":745,"stroke":78,"strokeWidth":747,"style":748,"markerEnd":749},"348.0","73.5","410.0","1.5","stroke-opacity:0.6","url(#fa5b310978)",[81,751,184],{"x":752,"y":753,"fill":134,"style":754,"textAnchor":106},"380.0","66.5","font-size:11.5px;font-weight:700",[284,756],{"x1":730,"y1":757,"x2":730,"y2":758,"stroke":78,"strokeWidth":747,"style":748,"markerEnd":749},"91.0","125.0",[81,760,764],{"x":761,"y":762,"fill":763,"style":754},"196.0","113.0","#51617a","no",[58,766],{"x":83,"y":767,"width":725,"height":726,"rx":727,"fill":78,"stroke":78,"style":91},"127.0",[81,769,771],{"x":730,"y":770,"fill":78,"style":732,"textAnchor":106},"149.0","Is it an effect tied to scroll position?",[58,773],{"x":736,"y":767,"width":725,"height":726,"rx":727,"fill":134,"stroke":134,"style":135},[81,775,776],{"x":739,"y":770,"fill":78,"style":740,"textAnchor":106},"Scroll-driven animations",[284,778],{"x1":744,"y1":779,"x2":746,"y2":779,"stroke":78,"strokeWidth":747,"style":748,"markerEnd":749},"144.5",[81,781,184],{"x":752,"y":782,"fill":134,"style":754,"textAnchor":106},"137.5",[284,784],{"x1":730,"y1":785,"x2":730,"y2":761,"stroke":78,"strokeWidth":747,"style":748,"markerEnd":749},"162.0",[81,787,764],{"x":761,"y":788,"fill":763,"style":754},"184.0",[58,790],{"x":83,"y":791,"width":725,"height":726,"rx":727,"fill":78,"stroke":78,"style":91},"198.0",[81,793,795],{"x":730,"y":794,"fill":78,"style":732,"textAnchor":106},"220.0","Does it react to an element entering view?",[58,797],{"x":736,"y":791,"width":725,"height":726,"rx":727,"fill":134,"stroke":134,"style":135},[81,799,44],{"x":739,"y":794,"fill":78,"style":740,"textAnchor":106},[284,801],{"x1":744,"y1":802,"x2":746,"y2":802,"stroke":78,"strokeWidth":747,"style":748,"markerEnd":749},"215.5",[81,804,184],{"x":752,"y":805,"fill":134,"style":754,"textAnchor":106},"208.5",[284,807],{"x1":730,"y1":808,"x2":730,"y2":809,"stroke":78,"strokeWidth":747,"style":748,"markerEnd":749},"233.0","267.0",[81,811,764],{"x":761,"y":812,"fill":763,"style":754},"255.0",[58,814],{"x":83,"y":815,"width":725,"height":726,"rx":727,"fill":78,"stroke":78,"style":91},"269.0",[81,817,819],{"x":730,"y":818,"fill":78,"style":732,"textAnchor":106},"291.0","Does script logic need an element's size?",[58,821],{"x":736,"y":815,"width":725,"height":726,"rx":727,"fill":134,"stroke":134,"style":135},[81,823,41],{"x":739,"y":818,"fill":78,"style":740,"textAnchor":106},[284,825],{"x1":744,"y1":826,"x2":746,"y2":826,"stroke":78,"strokeWidth":747,"style":748,"markerEnd":749},"286.5",[81,828,184],{"x":752,"y":829,"fill":134,"style":754,"textAnchor":106},"279.5",[284,831],{"x1":730,"y1":832,"x2":730,"y2":833,"stroke":78,"strokeWidth":747,"style":748,"markerEnd":749},"304.0","338.0",[81,835,764],{"x":761,"y":836,"fill":763,"style":754},"326.0",[58,838],{"x":83,"y":839,"width":725,"height":84,"rx":727,"fill":125,"stroke":126,"style":127},"340.0",[81,841,843],{"x":730,"y":842,"fill":78,"style":740,"textAnchor":106},"361.5","Keep a passive handler; read first, write in rAF",[191,845,847],{"id":846},"avoiding-resizeobserver-loops","Avoiding ResizeObserver Loops",[14,849,850],{},"A ResizeObserver callback that changes the size of an observed element can trigger itself again. Browsers process such changes within the same frame up to a depth limit, then report \"ResizeObserver loop completed with undelivered notifications\" and defer the rest to the next frame. This is a safety mechanism, not a crash, but it signals wasted work and possible flicker. Avoid it by observing a container whose size does not depend on what the callback changes, by changing styles of children rather than the observed element, or by applying hysteresis (different thresholds for switching on and off) so a size near a boundary does not oscillate.",[273,852,854],{"className":275,"code":853,"language":277,"meta":278,"style":278},"let compact = false;\nconst ro = new ResizeObserver(([entry]) => {\n  const w = entry.contentBoxSize[0].inlineSize;\n  const next = compact ? w \u003C 440 : w \u003C 400;    \u002F\u002F hysteresis band 400-440px\n  if (next !== compact) { compact = next; entry.target.classList.toggle('compact', compact); }\n});\n\u002F\u002F trade-off: hysteresis means the breakpoint differs slightly depending on the\n\u002F\u002F direction of resizing, which users rarely notice.\n",[29,855,856,873,898,915,953,981,985,990],{"__ignoreMap":278},[282,857,858,861,864,867,870],{"class":284,"line":285},[282,859,860],{"class":288},"let",[282,862,863],{"class":306}," compact ",[282,865,866],{"class":288},"=",[282,868,869],{"class":292}," false",[282,871,872],{"class":306},";\n",[282,874,875,877,879,881,883,885,888,891,894,896],{"class":284,"line":323},[282,876,289],{"class":288},[282,878,293],{"class":292},[282,880,296],{"class":288},[282,882,299],{"class":288},[282,884,303],{"class":302},[282,886,887],{"class":306},"(([",[282,889,890],{"class":310},"entry",[282,892,893],{"class":306},"]) ",[282,895,317],{"class":288},[282,897,320],{"class":306},[282,899,900,903,906,908,910,912],{"class":284,"line":343},[282,901,902],{"class":288},"  const",[282,904,905],{"class":292}," w",[282,907,296],{"class":288},[282,909,354],{"class":306},[282,911,62],{"class":292},[282,913,914],{"class":306},"].inlineSize;\n",[282,916,917,919,922,924,926,929,932,934,937,940,942,944,947,950],{"class":284,"line":366},[282,918,902],{"class":288},[282,920,921],{"class":292}," next",[282,923,296],{"class":288},[282,925,863],{"class":306},[282,927,928],{"class":288},"?",[282,930,931],{"class":306}," w ",[282,933,385],{"class":288},[282,935,936],{"class":292}," 440",[282,938,939],{"class":288}," :",[282,941,931],{"class":306},[282,943,385],{"class":288},[282,945,946],{"class":292}," 400",[282,948,949],{"class":306},";    ",[282,951,952],{"class":362},"\u002F\u002F hysteresis band 400-440px\n",[282,954,955,958,961,964,967,969,972,974,976,978],{"class":284,"line":394},[282,956,957],{"class":288},"  if",[282,959,960],{"class":306}," (next ",[282,962,963],{"class":288},"!==",[282,965,966],{"class":306}," compact) { compact ",[282,968,866],{"class":288},[282,970,971],{"class":306}," next; entry.target.classList.",[282,973,372],{"class":302},[282,975,375],{"class":306},[282,977,379],{"class":378},[282,979,980],{"class":306},", compact); }\n",[282,982,983],{"class":284,"line":400},[282,984,403],{"class":306},[282,986,987],{"class":284,"line":406},[282,988,989],{"class":362},"\u002F\u002F trade-off: hysteresis means the breakpoint differs slightly depending on the\n",[282,991,992],{"class":284,"line":444},[282,993,994],{"class":362},"\u002F\u002F direction of resizing, which users rarely notice.\n",[191,996,998],{"id":997},"migrating-a-handler-step-by-step","Migrating a Handler Step by Step",[14,1000,1001],{},"Migrate one handler at a time. Start by writing down what the handler actually needs to know (an element's width, whether a section is visible, the scroll offset) and what it changes. Choose the tool from the decision above, implement it next to the old handler behind a flag, and compare behaviour on resize and scroll. Then remove the old listener and any throttle or debounce wrapper that existed only to make it affordable. Record a trace before and after, so the improvement is documented for the next person who wonders why the code looks different.",[191,1003,1005],{"id":1004},"common-mistakes","Common Mistakes",[196,1007,1008,1014,1020,1026],{},[199,1009,1010,1013],{},[202,1011,1012],{},"Wrapping observers in debounce."," Observers already coalesce; debouncing adds latency without benefit.",[199,1015,1016,1019],{},[202,1017,1018],{},"Reading layout inside observer callbacks."," Use the entry's sizes; additional reads can force layout after writes in the callback.",[199,1021,1022,1025],{},[202,1023,1024],{},"One observer per element."," Use one observer for many elements; it is more efficient.",[199,1027,1028,1031],{},[202,1029,1030],{},"Forgetting to disconnect."," Remove observers when components unmount to avoid leaks.",[191,1033,1035],{"id":1034},"edge-cases","Edge Cases",[14,1037,1038,1041,1042,1045],{},[202,1039,1040],{},"Window size itself."," For window-level breakpoints, ",[29,1043,1044],{},"matchMedia"," listeners fire only when a media query changes, cheaper than resize handlers.",[14,1047,1048,1051,1052,1055],{},[202,1049,1050],{},"Scroll position for progress bars."," Use scroll-driven animations (",[29,1053,1054],{},"animation-timeline: scroll()",") to avoid JavaScript entirely.",[14,1057,1058,1061,1062,1065],{},[202,1059,1060],{},"Virtual keyboards on mobile."," The ",[29,1063,1064],{},"visualViewport"," API's resize event reports keyboard changes; handle it with the same read-then-write discipline.",[14,1067,1068,1071],{},[202,1069,1070],{},"Older browsers."," All modern browsers support both observers; polyfills exist but rely on polling.",[191,1073,1075],{"id":1074},"faq","FAQ",[1077,1078,1081,1085],"details",{"className":1079},[1080],"faq-item",[1082,1083,1084],"summary",{},"Is ResizeObserver faster than a resize event listener?",[14,1086,1087],{},"Generally yes. It fires only when observed elements actually change size, delivers the size without forcing layout, and works per element.",[1077,1089,1091,1094],{"className":1090},[1080],[1082,1092,1093],{},"When does ResizeObserver fire?",[14,1095,1096],{},"After layout and before paint in each frame where an observed element's size changed. Callbacks see up-to-date sizes.",[1077,1098,1100,1103],{"className":1099},[1080],[1082,1101,1102],{},"Can IntersectionObserver replace all scroll handlers?",[14,1104,1105],{},"Most of them — visibility, lazy loading, active section tracking, infinite scroll triggers. Effects that must follow scroll position precisely are better done with scroll-driven animations.",[1077,1107,1109,1112],{"className":1108},[1080],[1082,1110,1111],{},"What does \"ResizeObserver loop completed with undelivered notifications\" mean?",[14,1113,1114],{},"A callback changed sizes of observed elements, triggering further notifications that the browser deferred. It is not fatal but indicates a feedback loop to fix.",[1077,1116,1118,1121],{"className":1117},[1080],[1082,1119,1120],{},"Should scroll listeners be passive?",[14,1122,1123,1124,1127],{},"Yes, unless they call ",[29,1125,1126],{},"preventDefault()",". Passive listeners let the browser scroll without waiting for the handler.",[1077,1129,1131,1134],{"className":1130},[1080],[1082,1132,1133],{},"Do container queries replace ResizeObserver?",[14,1135,1136],{},"For styling based on container size, yes, without any JavaScript. ResizeObserver is still needed when script logic depends on size, such as choosing how many chart labels to render.",[1077,1138,1140,1143],{"className":1139},[1080],[1082,1141,1142],{},"Does IntersectionObserver work inside scrollable containers?",[14,1144,1145,1146,1149],{},"Yes. Set the ",[29,1147,1148],{},"root"," option to the scrollable container, so intersections are computed against it rather than the viewport.",[191,1151,1153],{"id":1152},"related","Related",[196,1155,1156,1163,1170],{},[199,1157,1158,1162],{},[18,1159,1161],{"href":1160},"\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fbatching-dom-reads-and-writes\u002F","Batching DOM reads and writes"," — when you must read and write in script.",[199,1164,1165,1169],{},[18,1166,1168],{"href":1167},"\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fscroll-driven-animations-without-javascript\u002F","Scroll-driven animations without JavaScript"," — replace scroll-linked effects.",[199,1171,1172,1176],{},[18,1173,1175],{"href":1174},"\u002Fimage-media-optimization\u002Flazy-loading-images-without-hurting-lcp\u002Fnative-lazy-loading-vs-intersection-observer\u002F","Native lazy loading vs IntersectionObserver"," — observers for media loading.",[1178,1179,1181],"script",{"type":1180},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"ResizeObserver vs Resize and Scroll Handlers\",\n  \"description\": \"When and how to replace resize and scroll event listeners with ResizeObserver and IntersectionObserver, avoiding forced layout and main-thread churn.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Replace resize-plus-measure with ResizeObserver\",\n      \"text\": \"Replace resize-plus-measure with ResizeObserver\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Replace scroll-plus-geometry with IntersectionObserver\",\n      \"text\": \"Replace scroll-plus-geometry with IntersectionObserver\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Use CSS where possible\",\n      \"text\": \"Container queries replace most \\\"component width\\\" scripts; position: sticky replaces header-fixing scripts; scroll-driven animations replace parallax and progress-bar scroll handlers.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Keep any remaining scroll handlers passive and cheap\",\n      \"text\": \"If a scroll handler is unavoidable, register it as { passive: true }, read only scrollY (cheap when layout is clean), and defer writes to requestAnimationFrame.\"\n    }\n  ]\n}\n",[1178,1183,1184],{"type":1180},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"ResizeObserver vs Resize and Scroll Handlers\",\n  \"description\": \"When and how to replace resize and scroll event listeners with ResizeObserver and IntersectionObserver, avoiding forced layout and main-thread churn.\",\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\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fresizeobserver-vs-resize-and-scroll-handlers\u002F\"\n  }\n}\n",[1178,1186,1187],{"type":1180},"\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\": \"Rendering & CSS Performance\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Frendering-css-performance\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Layout Thrashing & Forced Reflow\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"ResizeObserver vs Resize and Scroll Handlers\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fresizeobserver-vs-resize-and-scroll-handlers\u002F\"\n    }\n  ]\n}\n",[1189,1190,1191],"style",{},"html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}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 .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}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":278,"searchDepth":323,"depth":323,"links":1193},[1194,1195,1196,1202,1203,1204,1205,1206,1207,1208,1209],{"id":193,"depth":323,"text":194},{"id":233,"depth":323,"text":234},{"id":265,"depth":323,"text":266,"children":1197},[1198,1199,1200,1201],{"id":270,"depth":343,"text":271},{"id":456,"depth":343,"text":457},{"id":579,"depth":343,"text":580},{"id":590,"depth":343,"text":591},{"id":672,"depth":323,"text":673},{"id":679,"depth":323,"text":680},{"id":846,"depth":323,"text":847},{"id":997,"depth":323,"text":998},{"id":1004,"depth":323,"text":1005},{"id":1034,"depth":323,"text":1035},{"id":1074,"depth":323,"text":1075},{"id":1152,"depth":323,"text":1153},"When and how to replace resize and scroll event listeners with ResizeObserver and IntersectionObserver, avoiding forced layout and main-thread churn.","md",{"slug":12,"type":1213,"breadcrumb":1214,"datePublished":1222,"dateModified":1222},"article",[1215,1218,1219,1220],{"name":1216,"url":1217},"Home","\u002F",{"name":26,"url":25},{"name":21,"url":20},{"name":5,"url":1221},"\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fresizeobserver-vs-resize-and-scroll-handlers\u002F","2026-10-06",true,"\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fresizeobserver-vs-resize-and-scroll-handlers",{"title":1226,"description":1227},"ResizeObserver vs Resize and Scroll Event Handlers","Replace resize and scroll handlers that measure the DOM with ResizeObserver and IntersectionObserver to stop forced reflows, jank and handler-heavy INP.","rendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fresizeobserver-vs-resize-and-scroll-handlers\u002Findex","3TZ9lAky0tt4d626yNCLjnMrbX_7EyUpAKjdMe16sQc",[1231,1235],{"title":1232,"path":1233,"stem":1234},"Finding Forced Reflows in the Performance Panel","\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Ffinding-forced-reflows-in-the-performance-panel","rendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Ffinding-forced-reflows-in-the-performance-panel\u002Findex",null,1791308077316]