[{"data":1,"prerenderedAt":1241},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more":1232},{"id":4,"title":5,"body":6,"description":1212,"extension":1213,"meta":1214,"navigation":1225,"path":1226,"seo":1227,"stem":1230,"__hash__":1231},"content\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more\u002Findex.md","Fixing CLS on Infinite Scroll and Load More",{"type":7,"value":8,"toc":1198},"minimark",[9,14,29,32,126,131,179,183,189,195,201,207,318,322,327,330,479,482,486,559,562,566,569,665,668,672,675,761,764,843,847,853,1093,1096,1100,1103,1107,1124,1137,1156,1160,1183,1188,1191,1194],[10,11,13],"h1",{"id":12},"how-to-fix-layout-shift-on-infinite-scroll-and-load-more-lists","How to Fix Layout Shift on Infinite Scroll and \"Load More\" Lists",[15,16,17,18,23,24,28],"p",{},"This guide tackles growing lists, within ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002F","Reducing Cumulative Layout Shift (CLS)"," in ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",". Feeds, search results and product grids that load more content as the user scrolls are a common source of CLS that only appears in long sessions — which is why it shows up in field data but rarely in a single lab load.",[15,30,31],{},"Appending items to the end of a list should not shift anything the user can see: new content appears below. In practice, three things go wrong. The footer is visible when new items arrive and gets pushed down. A loading spinner is inserted and removed, moving content around it. And items already in view resize as their images load late. Each of these happens without a discrete user input — scrolling does not count as one — so every shift contributes to CLS.",[15,33,34],{},[35,36,42,43,42,50,42,54,42,57,42,66,42,72,42,80,42,86,42,92,42,98,42,102,42,105,42,108,42,112,42,115,42,119,42,123,42],"svg",{"viewBox":37,"width":38,"role":39,"ariaLabel":40,"style":41},"0 0 760 268","100%","img","Vertical layers of an infinite scrolling page showing the loaded items, the sentinel and loading slot, and the footer, with the shift risk of each.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[44,45],"rect",{"className":46,"x":48,"y":48,"width":38,"height":38,"fill":49},[47],"svg-canvas","0","#ffffff",[51,52,53],"title",{},"Layout regions of an infinite list",[55,56,40],"desc",{},[44,58],{"x":59,"y":59,"width":60,"height":61,"rx":62,"fill":63,"stroke":64,"style":65},"1","758","266","10","none","currentColor","stroke-opacity:0.18",[67,68,53],"text",{"x":69,"y":70,"fill":64,"style":71},"28.0","34.0","font-size:16px;font-weight:700",[44,73],{"x":69,"y":74,"width":75,"height":76,"rx":77,"fill":78,"stroke":78,"style":79},"56.0","704.0","40.0","6","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[67,81,85],{"x":82,"y":83,"fill":64,"style":84},"42.0","80.5","font-size:13px;font-weight:700","Loaded items",[67,87,91],{"x":88,"y":83,"fill":64,"style":89,"textAnchor":90},"194.6","font-size:12px","start","safe if every item has reserved media dimensions",[44,93],{"x":69,"y":94,"width":75,"height":76,"rx":77,"fill":95,"stroke":96,"style":97},"106.0","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[67,99,101],{"x":82,"y":100,"fill":64,"style":84},"130.5","Sentinel + loading slot",[67,103,104],{"x":88,"y":100,"fill":64,"style":89,"textAnchor":90},"fixed-height slot: spinner appears inside it, never adds height",[44,106],{"x":69,"y":107,"width":75,"height":76,"rx":77,"fill":78,"stroke":78,"style":79},"156.0",[67,109,111],{"x":82,"y":110,"fill":64,"style":84},"180.5","Newly appended page",[67,113,114],{"x":88,"y":110,"fill":64,"style":89,"textAnchor":90},"inserted above the slot, below the viewport if prefetched early",[44,116],{"x":69,"y":117,"width":75,"height":76,"rx":77,"fill":64,"stroke":64,"style":118},"206.0","fill-opacity:0.06;stroke-opacity:0.4",[67,120,122],{"x":82,"y":121,"fill":64,"style":84},"230.5","Footer",[67,124,125],{"x":88,"y":121,"fill":64,"style":89,"textAnchor":90},"the usual victim — moves on every append if visible",[127,128,130],"h2",{"id":129},"rapid-diagnosis","Rapid Diagnosis",[132,133,134,142,148,154,173],"ul",{},[135,136,137,141],"li",{},[138,139,140],"strong",{},"Scroll slowly through several pages"," in a recorded trace with the Layout Shift Regions overlay on. Note whether flashes happen at the bottom (footer, spinner) or in the middle (items resizing).",[135,143,144,147],{},[138,145,146],{},"Check when the next page is requested."," If the request starts only when the sentinel reaches the viewport, new items arrive while the bottom of the list — and the footer — is visible.",[135,149,150,153],{},[138,151,152],{},"Inspect the loading indicator."," Is it inserted into the DOM when loading starts and removed when it ends? Each insertion and removal changes height.",[135,155,156,159,160,164,165,168,169,172],{},[138,157,158],{},"Check item media."," Images in appended items without ",[161,162,163],"code",{},"width","\u002F",[161,166,167],{},"height"," or ",[161,170,171],{},"aspect-ratio"," collapse and then expand.",[135,174,175,178],{},[138,176,177],{},"Check \"Load more\" buttons."," If the button's click triggers loading, shifts within 500ms are excluded; content that arrives after that window is not.",[127,180,182],{"id":181},"root-cause-analysis","Root Cause Analysis",[15,184,185,188],{},[138,186,187],{},"1. Visible footer pushed down."," When the user reaches the end, the footer is in view. New items inserted above it push it down — a large shift of a full-width element.",[15,190,191,194],{},[138,192,193],{},"2. Spinner insertion and removal."," A spinner element added below the list adds its height; removing it after items render removes that height again. Two shifts per page load.",[15,196,197,200],{},[138,198,199],{},"3. Late-loading media inside appended items."," Items arrive with text first; images load afterwards and expand cards that are already visible.",[15,202,203,206],{},[138,204,205],{},"4. Load-more responses outside the input window."," A \"Load more\" click excuses shifts for 500ms. A slow API response arriving after 800ms inserts content with no recent input — counted.",[15,208,209],{},[35,210,42,213,42,216,42,219,42,221,42,224,42,226,42,236,42,241,42,248,42,252,42,257,42,264,42,268,42,273,42,278,42,284,42,288,42,292,42,296,42,300,42,304,42,308,42,312,42],{"viewBox":211,"width":38,"role":39,"ariaLabel":212,"style":41},"0 0 760 238","Timeline of a load more click where the API responds after the 500 millisecond input exclusion, so inserting the new items counts as a layout shift.",[44,214],{"className":215,"x":48,"y":48,"width":38,"height":38,"fill":49},[47],[51,217,218],{},"A load-more click with a slow response",[55,220,212],{},[44,222],{"x":59,"y":59,"width":60,"height":223,"rx":62,"fill":63,"stroke":64,"style":65},"236",[67,225,218],{"x":69,"y":70,"fill":64,"style":71},[227,228],"line",{"x1":229,"y1":230,"x2":229,"y2":231,"stroke":96,"strokeWidth":232,"strokeDashArray":233,"style":235},"318.1","72.0","194.0","1.5",[234,234],"4","stroke-opacity:0.9",[67,237,240],{"x":69,"y":238,"fill":64,"style":239},"96.0","font-size:12.5px;font-weight:700","User",[44,242],{"x":243,"y":244,"width":245,"height":246,"rx":247,"fill":78,"stroke":78,"style":79},"92.3","78.0","25.7","26.0","3",[67,249,251],{"x":69,"y":250,"fill":64,"style":239},"134.0","Network",[44,253],{"x":254,"y":255,"width":256,"height":246,"rx":247,"fill":64,"stroke":64,"style":118},"119.5","116.0","388.3",[67,258,263],{"x":259,"y":260,"fill":64,"style":261,"textAnchor":262},"313.6","133.5","font-size:11.5px","middle","GET \u002Fapi\u002Fitems?page=3",[67,265,267],{"x":69,"y":266,"fill":64,"style":239},"172.0","Layout",[44,269],{"x":270,"y":271,"width":272,"height":246,"rx":247,"fill":95,"stroke":96,"style":97},"509.2","154.0","52.9",[227,274],{"x1":275,"y1":231,"x2":276,"y2":231,"stroke":64,"style":277},"91.5","726.0","stroke-opacity:0.4",[67,279,283],{"x":280,"y":281,"fill":282,"style":261,"textAnchor":262},"102.2","211.0","#51617a","0ms",[67,285,287],{"x":286,"y":281,"fill":282,"style":261,"textAnchor":262},"182.2","200ms",[67,289,291],{"x":290,"y":281,"fill":282,"style":261,"textAnchor":262},"272.8","400ms",[67,293,295],{"x":294,"y":281,"fill":282,"style":261,"textAnchor":262},"363.4","600ms",[67,297,299],{"x":298,"y":281,"fill":282,"style":261,"textAnchor":262},"454.1","800ms",[67,301,303],{"x":302,"y":281,"fill":282,"style":261,"textAnchor":262},"544.7","1000ms",[67,305,307],{"x":306,"y":281,"fill":282,"style":261,"textAnchor":262},"635.4","1200ms",[67,309,311],{"x":310,"y":281,"fill":282,"style":261,"textAnchor":262},"712.3","1400ms",[67,313,317],{"x":229,"y":314,"fill":315,"style":316,"textAnchor":262},"68.0","#8a6608","font-size:12px;font-weight:700","500ms exclusion ends",[127,319,321],{"id":320},"step-by-step-resolution","Step-by-Step Resolution",[323,324,326],"h3",{"id":325},"_1-prefetch-the-next-page-before-the-user-reaches-the-end","1. Prefetch the next page before the user reaches the end",[15,328,329],{},"Trigger loading well before the sentinel is visible, so new items are inserted below the viewport, where they cannot shift anything visible.",[331,332,337],"pre",{"className":333,"code":334,"language":335,"meta":336,"style":336},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","const sentinel = document.querySelector('#list-sentinel');\nconst io = new IntersectionObserver(([entry]) => {\n  if (entry.isIntersecting && !loading && hasMore) loadNextPage();\n}, { rootMargin: '0px 0px 1500px 0px' });   \u002F\u002F fire ~1.5 screens early\nio.observe(sentinel);\n\u002F\u002F trade-off: a large rootMargin loads pages the user may never scroll to,\n\u002F\u002F costing bandwidth and API capacity. Tune it per device: smaller on slow\n\u002F\u002F connections where data cost matters more than smoothness.\n","javascript","",[161,338,339,372,404,433,449,461,467,473],{"__ignoreMap":336},[340,341,343,347,351,354,358,362,365,369],"span",{"class":227,"line":342},1,[340,344,346],{"class":345},"sPARh","const",[340,348,350],{"class":349},"sPXB4"," sentinel",[340,352,353],{"class":345}," =",[340,355,357],{"class":356},"saISM"," document.",[340,359,361],{"class":360},"smZ65","querySelector",[340,363,364],{"class":356},"(",[340,366,368],{"class":367},"sZ8jY","'#list-sentinel'",[340,370,371],{"class":356},");\n",[340,373,375,377,380,382,385,388,391,395,398,401],{"class":227,"line":374},2,[340,376,346],{"class":345},[340,378,379],{"class":349}," io",[340,381,353],{"class":345},[340,383,384],{"class":345}," new",[340,386,387],{"class":360}," IntersectionObserver",[340,389,390],{"class":356},"(([",[340,392,394],{"class":393},"sQw3B","entry",[340,396,397],{"class":356},"]) ",[340,399,400],{"class":345},"=>",[340,402,403],{"class":356}," {\n",[340,405,407,410,413,416,419,422,424,427,430],{"class":227,"line":406},3,[340,408,409],{"class":345},"  if",[340,411,412],{"class":356}," (entry.isIntersecting ",[340,414,415],{"class":345},"&&",[340,417,418],{"class":345}," !",[340,420,421],{"class":356},"loading ",[340,423,415],{"class":345},[340,425,426],{"class":356}," hasMore) ",[340,428,429],{"class":360},"loadNextPage",[340,431,432],{"class":356},"();\n",[340,434,436,439,442,445],{"class":227,"line":435},4,[340,437,438],{"class":356},"}, { rootMargin: ",[340,440,441],{"class":367},"'0px 0px 1500px 0px'",[340,443,444],{"class":356}," });   ",[340,446,448],{"class":447},"sjfSM","\u002F\u002F fire ~1.5 screens early\n",[340,450,452,455,458],{"class":227,"line":451},5,[340,453,454],{"class":356},"io.",[340,456,457],{"class":360},"observe",[340,459,460],{"class":356},"(sentinel);\n",[340,462,464],{"class":227,"line":463},6,[340,465,466],{"class":447},"\u002F\u002F trade-off: a large rootMargin loads pages the user may never scroll to,\n",[340,468,470],{"class":227,"line":469},7,[340,471,472],{"class":447},"\u002F\u002F costing bandwidth and API capacity. Tune it per device: smaller on slow\n",[340,474,476],{"class":227,"line":475},8,[340,477,478],{"class":447},"\u002F\u002F connections where data cost matters more than smoothness.\n",[15,480,481],{},"Expected outcome: in normal scrolling, appends happen off-screen and generate no visible shifts.",[323,483,485],{"id":484},"_2-give-the-loading-indicator-a-permanent-fixed-height-slot","2. Give the loading indicator a permanent, fixed-height slot",[331,487,491],{"className":488,"code":489,"language":490,"meta":336,"style":336},"language-html shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u003Cdiv class=\"list-status\" aria-live=\"polite\" style=\"min-height: 4rem\">\n  \u003C!-- spinner or \"Loading more…\" text appears here; the slot never changes height -->\n\u003C\u002Fdiv>\n\u003C!-- trade-off: the slot occupies space even when idle. Use it for the\n     \"End of results\" message once there are no more pages, so the space\n     always carries meaning. -->\n","html",[161,492,493,530,535,544,549,554],{"__ignoreMap":336},[340,494,495,498,502,505,508,511,514,516,519,522,524,527],{"class":227,"line":342},[340,496,497],{"class":356},"\u003C",[340,499,501],{"class":500},"sZBmE","div",[340,503,504],{"class":349}," class",[340,506,507],{"class":356},"=",[340,509,510],{"class":367},"\"list-status\"",[340,512,513],{"class":349}," aria-live",[340,515,507],{"class":356},[340,517,518],{"class":367},"\"polite\"",[340,520,521],{"class":349}," style",[340,523,507],{"class":356},[340,525,526],{"class":367},"\"min-height: 4rem\"",[340,528,529],{"class":356},">\n",[340,531,532],{"class":227,"line":374},[340,533,534],{"class":447},"  \u003C!-- spinner or \"Loading more…\" text appears here; the slot never changes height -->\n",[340,536,537,540,542],{"class":227,"line":406},[340,538,539],{"class":356},"\u003C\u002F",[340,541,501],{"class":500},[340,543,529],{"class":356},[340,545,546],{"class":227,"line":435},[340,547,548],{"class":447},"\u003C!-- trade-off: the slot occupies space even when idle. Use it for the\n",[340,550,551],{"class":227,"line":451},[340,552,553],{"class":447},"     \"End of results\" message once there are no more pages, so the space\n",[340,555,556],{"class":227,"line":463},[340,557,558],{"class":447},"     always carries meaning. -->\n",[15,560,561],{},"Expected outcome: no shifts from spinner insertion or removal.",[323,563,565],{"id":564},"_3-keep-the-footer-out-of-the-shift-path","3. Keep the footer out of the shift path",[15,567,568],{},"For true infinite scroll, the footer is unreachable in practice — do not render it under the list, or move its links into a sidebar or a \"Load more\" pattern. For load-more lists, the footer is visible only when the user is at the end; inserting new items pushes it down, but if the click triggered the load and items arrive within 500ms, it is excluded.",[331,570,572],{"className":333,"code":571,"language":335,"meta":336,"style":336},"async function onLoadMoreClick() {\n  const res = await fetch(nextUrl, { priority: 'high' });   \u002F\u002F fast path for user-initiated loads\n  const items = await res.json();\n  renderItems(items);           \u002F\u002F insert within the input window where possible\n}\n\u002F\u002F trade-off: you cannot guarantee a 500ms response. For slow APIs, insert the\n\u002F\u002F new items' reserved slots immediately on click (skeletons of the right size)\n\u002F\u002F so the layout change happens inside the exclusion window, then fill them.\n",[161,573,574,588,615,634,645,650,655,660],{"__ignoreMap":336},[340,575,576,579,582,585],{"class":227,"line":342},[340,577,578],{"class":345},"async",[340,580,581],{"class":345}," function",[340,583,584],{"class":360}," onLoadMoreClick",[340,586,587],{"class":356},"() {\n",[340,589,590,593,596,598,601,604,607,610,612],{"class":227,"line":374},[340,591,592],{"class":345},"  const",[340,594,595],{"class":349}," res",[340,597,353],{"class":345},[340,599,600],{"class":345}," await",[340,602,603],{"class":360}," fetch",[340,605,606],{"class":356},"(nextUrl, { priority: ",[340,608,609],{"class":367},"'high'",[340,611,444],{"class":356},[340,613,614],{"class":447},"\u002F\u002F fast path for user-initiated loads\n",[340,616,617,619,622,624,626,629,632],{"class":227,"line":406},[340,618,592],{"class":345},[340,620,621],{"class":349}," items",[340,623,353],{"class":345},[340,625,600],{"class":345},[340,627,628],{"class":356}," res.",[340,630,631],{"class":360},"json",[340,633,432],{"class":356},[340,635,636,639,642],{"class":227,"line":435},[340,637,638],{"class":360},"  renderItems",[340,640,641],{"class":356},"(items);           ",[340,643,644],{"class":447},"\u002F\u002F insert within the input window where possible\n",[340,646,647],{"class":227,"line":451},[340,648,649],{"class":356},"}\n",[340,651,652],{"class":227,"line":463},[340,653,654],{"class":447},"\u002F\u002F trade-off: you cannot guarantee a 500ms response. For slow APIs, insert the\n",[340,656,657],{"class":227,"line":469},[340,658,659],{"class":447},"\u002F\u002F new items' reserved slots immediately on click (skeletons of the right size)\n",[340,661,662],{"class":227,"line":475},[340,663,664],{"class":447},"\u002F\u002F so the layout change happens inside the exclusion window, then fill them.\n",[15,666,667],{},"Expected outcome: user-initiated appends land inside the exclusion window, or reserve their space there.",[323,669,671],{"id":670},"_4-reserve-dimensions-for-every-items-media","4. Reserve dimensions for every item's media",[15,673,674],{},"Every image, video and embed in list items needs intrinsic dimensions from the API response so its box is correct before it loads.",[331,676,680],{"className":677,"code":678,"language":679,"meta":336,"style":336},"language-jsx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u003Cimg src={item.image.url} width={item.image.width} height={item.image.height}\n     loading=\"lazy\" decoding=\"async\" alt={item.image.alt} \u002F>\n\u002F\u002F trade-off: requires your API to return image dimensions. If it cannot, use a\n\u002F\u002F fixed aspect-ratio container and object-fit: cover, accepting some cropping.\n","jsx",[161,681,682,720,751,756],{"__ignoreMap":336},[340,683,684,686,688,691,694,697,700,703,705,708,710,713,715,718],{"class":227,"line":342},[340,685,497],{"class":356},[340,687,39],{"class":500},[340,689,690],{"class":349}," src",[340,692,693],{"class":345},"={",[340,695,696],{"class":356},"item.image.url",[340,698,699],{"class":345},"}",[340,701,702],{"class":349}," width",[340,704,693],{"class":345},[340,706,707],{"class":356},"item.image.width",[340,709,699],{"class":345},[340,711,712],{"class":349}," height",[340,714,693],{"class":345},[340,716,717],{"class":356},"item.image.height",[340,719,649],{"class":345},[340,721,722,725,727,730,733,735,738,741,743,746,748],{"class":227,"line":374},[340,723,724],{"class":349},"     loading",[340,726,507],{"class":345},[340,728,729],{"class":367},"\"lazy\"",[340,731,732],{"class":349}," decoding",[340,734,507],{"class":345},[340,736,737],{"class":367},"\"async\"",[340,739,740],{"class":349}," alt",[340,742,693],{"class":345},[340,744,745],{"class":356},"item.image.alt",[340,747,699],{"class":345},[340,749,750],{"class":356}," \u002F>\n",[340,752,753],{"class":227,"line":406},[340,754,755],{"class":447},"\u002F\u002F trade-off: requires your API to return image dimensions. If it cannot, use a\n",[340,757,758],{"class":227,"line":435},[340,759,760],{"class":447},"\u002F\u002F fixed aspect-ratio container and object-fit: cover, accepting some cropping.\n",[15,762,763],{},"Expected outcome: appended items never change size after insertion.",[15,765,766],{},[35,767,42,770,42,773,42,776,42,778,42,781,42,783,42,787,42,794,42,800,42,805,42,809,42,813,42,817,42,820,42,824,42,828,42,832,42,836,42,840,42],{"viewBox":768,"width":38,"role":39,"ariaLabel":769,"style":41},"0 0 760 216","Bar chart of CLS per cause in a 20-page infinite scroll session before fixes, and the total after fixes.",[44,771],{"className":772,"x":48,"y":48,"width":38,"height":38,"fill":49},[47],[51,774,775],{},"CLS on a long search session before and after",[55,777,769],{},[44,779],{"x":59,"y":59,"width":60,"height":780,"rx":62,"fill":63,"stroke":64,"style":65},"214",[67,782,775],{"x":69,"y":70,"fill":64,"style":71},[227,784],{"x1":785,"y1":230,"x2":785,"y2":231,"stroke":96,"strokeWidth":232,"strokeDashArray":786,"style":235},"668.0",[234,234],[67,788,793],{"x":789,"y":790,"fill":64,"style":791,"textAnchor":792},"157.1","92.0","font-size:13px","end","Footer pushed down",[44,795],{"x":796,"y":244,"width":797,"height":798,"rx":247,"fill":78,"stroke":78,"style":799},"169.1","449.0","19","fill-opacity:0.55;stroke-opacity:0.9",[67,801,804],{"x":802,"y":790,"fill":64,"style":803},"624.1","font-size:12px;font-weight:600","0.09",[67,806,808],{"x":789,"y":807,"fill":64,"style":791,"textAnchor":792},"123.0","Spinner insert\u002Fremove",[44,810],{"x":796,"y":811,"width":812,"height":798,"rx":247,"fill":78,"stroke":78,"style":799},"109.0","249.5",[67,814,816],{"x":815,"y":807,"fill":64,"style":803},"424.5","0.05",[67,818,819],{"x":789,"y":271,"fill":64,"style":791,"textAnchor":792},"Unsized item images",[44,821],{"x":796,"y":822,"width":823,"height":798,"rx":247,"fill":78,"stroke":78,"style":799},"140.0","349.3",[67,825,827],{"x":826,"y":271,"fill":64,"style":803},"524.3","0.07",[67,829,831],{"x":789,"y":830,"fill":64,"style":791,"textAnchor":792},"185.0","All fixes applied",[44,833],{"x":796,"y":834,"width":835,"height":798,"rx":247,"fill":78,"stroke":78,"style":799},"171.0","49.9",[67,837,839],{"x":838,"y":830,"fill":64,"style":803},"225.0","0.01",[67,841,842],{"x":785,"y":314,"fill":315,"style":316,"textAnchor":262},"CLS 0.1",[127,844,846],{"id":845},"verification","Verification",[15,848,849,850,852],{},"Script a long session in Playwright that scrolls slowly through ten pages, and assert the observed CLS stays below ",[161,851,816],{},". Run it on a throttled network so responses are slow enough to reproduce real conditions.",[331,854,856],{"className":333,"code":855,"language":335,"meta":336,"style":336},"await page.goto('\u002Fsearch?q=boots');\nawait page.evaluate(() => { window.__cls = 0; new PerformanceObserver((l) => {\n  for (const e of l.getEntries()) if (!e.hadRecentInput) window.__cls += e.value; })\n  .observe({ type: 'layout-shift', buffered: true }); });\nfor (let i = 0; i \u003C 10; i++) { await page.mouse.wheel(0, 1200); await page.waitForTimeout(800); }\nexpect(await page.evaluate(() => window.__cls)).toBeLessThan(0.05);\n\u002F\u002F trade-off: summing all shifts overstates CLS compared with the windowed\n\u002F\u002F definition, which makes this a conservative test — good for a CI gate.\n",[161,857,858,876,920,962,984,1054,1083,1088],{"__ignoreMap":336},[340,859,860,863,866,869,871,874],{"class":227,"line":342},[340,861,862],{"class":345},"await",[340,864,865],{"class":356}," page.",[340,867,868],{"class":360},"goto",[340,870,364],{"class":356},[340,872,873],{"class":367},"'\u002Fsearch?q=boots'",[340,875,371],{"class":356},[340,877,878,880,882,885,888,890,893,895,898,901,904,907,910,913,916,918],{"class":227,"line":374},[340,879,862],{"class":345},[340,881,865],{"class":356},[340,883,884],{"class":360},"evaluate",[340,886,887],{"class":356},"(() ",[340,889,400],{"class":345},[340,891,892],{"class":356}," { window.__cls ",[340,894,507],{"class":345},[340,896,897],{"class":349}," 0",[340,899,900],{"class":356},"; ",[340,902,903],{"class":345},"new",[340,905,906],{"class":360}," PerformanceObserver",[340,908,909],{"class":356},"((",[340,911,912],{"class":393},"l",[340,914,915],{"class":356},") ",[340,917,400],{"class":345},[340,919,403],{"class":356},[340,921,922,925,928,930,933,936,939,942,945,948,950,953,956,959],{"class":227,"line":406},[340,923,924],{"class":345},"  for",[340,926,927],{"class":356}," (",[340,929,346],{"class":345},[340,931,932],{"class":349}," e",[340,934,935],{"class":345}," of",[340,937,938],{"class":356}," l.",[340,940,941],{"class":360},"getEntries",[340,943,944],{"class":356},"()) ",[340,946,947],{"class":345},"if",[340,949,927],{"class":356},[340,951,952],{"class":345},"!",[340,954,955],{"class":356},"e.hadRecentInput) window.__cls ",[340,957,958],{"class":345},"+=",[340,960,961],{"class":356}," e.value; })\n",[340,963,964,967,969,972,975,978,981],{"class":227,"line":435},[340,965,966],{"class":356},"  .",[340,968,457],{"class":360},[340,970,971],{"class":356},"({ type: ",[340,973,974],{"class":367},"'layout-shift'",[340,976,977],{"class":356},", buffered: ",[340,979,980],{"class":349},"true",[340,982,983],{"class":356}," }); });\n",[340,985,986,989,991,994,997,999,1001,1004,1006,1009,1012,1015,1018,1020,1023,1026,1028,1030,1033,1036,1039,1041,1043,1046,1048,1051],{"class":227,"line":451},[340,987,988],{"class":345},"for",[340,990,927],{"class":356},[340,992,993],{"class":345},"let",[340,995,996],{"class":356}," i ",[340,998,507],{"class":345},[340,1000,897],{"class":349},[340,1002,1003],{"class":356},"; i ",[340,1005,497],{"class":345},[340,1007,1008],{"class":349}," 10",[340,1010,1011],{"class":356},"; i",[340,1013,1014],{"class":345},"++",[340,1016,1017],{"class":356},") { ",[340,1019,862],{"class":345},[340,1021,1022],{"class":356}," page.mouse.",[340,1024,1025],{"class":360},"wheel",[340,1027,364],{"class":356},[340,1029,48],{"class":349},[340,1031,1032],{"class":356},", ",[340,1034,1035],{"class":349},"1200",[340,1037,1038],{"class":356},"); ",[340,1040,862],{"class":345},[340,1042,865],{"class":356},[340,1044,1045],{"class":360},"waitForTimeout",[340,1047,364],{"class":356},[340,1049,1050],{"class":349},"800",[340,1052,1053],{"class":356},"); }\n",[340,1055,1056,1059,1061,1063,1065,1067,1069,1071,1074,1077,1079,1081],{"class":227,"line":463},[340,1057,1058],{"class":360},"expect",[340,1060,364],{"class":356},[340,1062,862],{"class":345},[340,1064,865],{"class":356},[340,1066,884],{"class":360},[340,1068,887],{"class":356},[340,1070,400],{"class":345},[340,1072,1073],{"class":356}," window.__cls)).",[340,1075,1076],{"class":360},"toBeLessThan",[340,1078,364],{"class":356},[340,1080,816],{"class":349},[340,1082,371],{"class":356},[340,1084,1085],{"class":227,"line":469},[340,1086,1087],{"class":447},"\u002F\u002F trade-off: summing all shifts overstates CLS compared with the windowed\n",[340,1089,1090],{"class":227,"line":475},[340,1091,1092],{"class":447},"\u002F\u002F definition, which makes this a conservative test — good for a CI gate.\n",[15,1094,1095],{},"In RUM, chart CLS by session length or scroll depth; after the fix, deep-scrolling sessions should no longer have markedly worse CLS than shallow ones.",[127,1097,1099],{"id":1098},"choosing-between-infinite-scroll-load-more-and-pagination","Choosing Between Infinite Scroll, Load More and Pagination",[15,1101,1102],{},"From a CLS standpoint, the three patterns differ in how much control you have. Pagination replaces the whole list on navigation — a new route — so shifts are confined to that view's initial render. \"Load more\" ties appends to a click, giving you the 500ms exclusion window to work within. Infinite scroll has no input at all, so it relies entirely on prefetching and space reservation to stay shift-free. Infinite scroll also has accessibility and footer-reachability costs. For search results and product listings, \"Load more\" with prefetching on hover of the button is often the best balance: users get continuity, the footer stays reachable, and every append is input-triggered.",[127,1104,1106],{"id":1105},"faq","FAQ",[1108,1109,1112,1116],"details",{"className":1110},[1111],"faq-item",[1113,1114,1115],"summary",{},"Does virtualisation cause layout shift?",[15,1117,1118,1119,1123],{},"Virtualised lists add and remove DOM nodes as you scroll, but they position items absolutely or with transforms inside a container whose total height is known, so visible content does not move. Problems arise when item heights are estimated and corrected after render — the correction moves items. Measure heights once and cache them, or use fixed heights. See ",[19,1120,1122],{"href":1121},"\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Fvirtualizing-long-lists-in-react\u002F","virtualizing long lists in React",".",[1108,1125,1127,1130],{"className":1126},[1111],[1113,1128,1129],{},"Is scrolling considered user input for CLS?",[15,1131,1132,1133,1136],{},"No. Only discrete inputs — clicks, taps, key presses — set ",[161,1134,1135],{},"hadRecentInput",". Scrolling is continuous and does not excuse shifts. That is why infinite scroll needs space reservation and early loading rather than relying on the exclusion window.",[1108,1138,1140,1143],{"className":1139},[1111],[1113,1141,1142],{},"Should new items fade in to soften their arrival?",[15,1144,1145,1146,1149,1150,168,1152,1155],{},"A fade with ",[161,1147,1148],{},"opacity"," is fine — it does not change layout, so it adds no shift. What matters is that the items' boxes are inserted at their final size in one step. Avoid entrance effects that animate ",[161,1151,167],{},[161,1153,1154],{},"max-height"," from zero, which turn one insertion into a shift on every frame, and avoid staggered insertions where each item is added separately over several hundred milliseconds.",[127,1157,1159],{"id":1158},"related","Related",[132,1161,1162,1169,1176],{},[135,1163,1164,1168],{},[19,1165,1167],{"href":1166},"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize\u002F","Fixing CLS from skeleton screens that resize"," — sizing the placeholders inserted on load.",[135,1170,1171,1175],{},[19,1172,1174],{"href":1173},"\u002Fimage-media-optimization\u002Flazy-loading-images-without-hurting-lcp\u002Fnative-lazy-loading-vs-intersection-observer\u002F","Native lazy loading vs IntersectionObserver"," — the loading strategy for appended item images.",[135,1177,1178,1182],{},[19,1179,1181],{"href":1180},"\u002Fcore-web-vitals-measurement\u002Fsoft-navigations-and-spa-metrics\u002Fwhy-spa-cls-accumulates-across-routes\u002F","Why SPA CLS accumulates across routes"," — long sessions and CLS windows.",[1184,1185,1187],"script",{"type":1186},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Fix Layout Shift on Infinite Scroll and \\\"Load More\\\" Lists\",\n  \"description\": \"Why appending items to long lists produces layout shift, which shifts are excused, and how to grow a list without moving what the user is looking at.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Prefetch the next page before the user reaches the end\",\n      \"text\": \"Trigger loading well before the sentinel is visible, so new items are inserted below the viewport, where they cannot shift anything visible.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Give the loading indicator a permanent, fixed-height slot\",\n      \"text\": \"Expected outcome: no shifts from spinner insertion or removal.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Keep the footer out of the shift path\",\n      \"text\": \"For true infinite scroll, the footer is unreachable in practice — do not render it under the list, or move its links into a sidebar or a \\\"Load more\\\" pattern.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Reserve dimensions for every item's media\",\n      \"text\": \"Every image, video and embed in list items needs intrinsic dimensions from the API response so its box is correct before it loads.\"\n    }\n  ]\n}\n",[1184,1189,1190],{"type":1186},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Fix Layout Shift on Infinite Scroll and \\\"Load More\\\" Lists\",\n  \"description\": \"Why appending items to long lists produces layout shift, which shifts are excused, and how to grow a list without moving what the user is looking at.\",\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\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more\u002F\"\n  }\n}\n",[1184,1192,1193],{"type":1186},"\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\": \"Reducing Cumulative Layout Shift (CLS)\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Fixing CLS on Infinite Scroll and Load More\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more\u002F\"\n    }\n  ]\n}\n",[1195,1196,1197],"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 .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}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 .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":336,"searchDepth":374,"depth":374,"links":1199},[1200,1201,1202,1208,1209,1210,1211],{"id":129,"depth":374,"text":130},{"id":181,"depth":374,"text":182},{"id":320,"depth":374,"text":321,"children":1203},[1204,1205,1206,1207],{"id":325,"depth":406,"text":326},{"id":484,"depth":406,"text":485},{"id":564,"depth":406,"text":565},{"id":670,"depth":406,"text":671},{"id":845,"depth":374,"text":846},{"id":1098,"depth":374,"text":1099},{"id":1105,"depth":374,"text":1106},{"id":1158,"depth":374,"text":1159},"Why appending items to long lists produces layout shift, which shifts are excused, and how to grow a list without moving what the user is looking at.","md",{"slug":1215,"type":1216,"breadcrumb":1217,"datePublished":1224,"dateModified":1224},"fixing-cls-on-infinite-scroll-and-load-more","article",[1218,1220,1221,1222],{"name":1219,"url":164},"Home",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1223},"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more\u002F","2026-10-06",true,"\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more",{"title":1228,"description":1229},"Fixing CLS on Infinite Scroll and Load More Lists","Infinite scroll and load-more lists shift content when new items, footers and spinners move. Reserve space, keep footers stable and keep CLS under 0.1.","core-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-on-infinite-scroll-and-load-more\u002Findex","rQ9Ui90CVgX6pmFMrRlITYinJN3QkwIPeRnCMKy8xL4",[1233,1237],{"title":1234,"path":1235,"stem":1236},"Fixing CLS from Skeleton Screens That Resize","\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize","core-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Ffixing-cls-from-skeleton-screens-that-resize\u002Findex",{"title":1238,"path":1239,"stem":1240},"Reserving Space for Embeds with aspect-ratio","\u002Fcore-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Freserving-space-for-embeds-with-aspect-ratio","core-web-vitals-measurement\u002Freducing-cumulative-layout-shift-cls\u002Freserving-space-for-embeds-with-aspect-ratio\u002Findex",1791308076787]