[{"data":1,"prerenderedAt":1274},["ShallowReactive",2],{"content:\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver":3,"surroundings:\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver":1266},{"id":4,"title":5,"body":6,"description":1247,"extension":1248,"meta":1249,"navigation":372,"path":1261,"seo":1262,"stem":1264,"__hash__":1265},"content\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002Findex.md","Deferring Below-the-Fold Hydration with IntersectionObserver",{"type":7,"value":8,"toc":1239},"minimark",[9,14,34,37,42,71,204,208,214,220,230,236,240,246,667,670,676,788,791,801,807,999,1002,1006,1009,1119,1122,1126,1138,1150,1168,1181,1190,1199,1203,1224,1229,1232,1235],[10,11,13],"h1",{"id":12},"how-to-defer-below-the-fold-hydration-with-intersectionobserver","How to Defer Below-the-Fold Hydration with IntersectionObserver",[15,16,17,18,23,24,28,29,33],"p",{},"This pattern belongs to ",[19,20,22],"a",{"href":21},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002F","Hydration & Partial Rehydration Strategies"," in ",[19,25,27],{"href":26},"\u002Fjavascript-bundle-optimization-code-splitting\u002F","JavaScript Bundle Optimization & Code Splitting",". Your page hydrates a comment thread, a related-products carousel and a newsletter form that sit two viewports below the fold — all before the user can tap the button at the top. Deferring that work is the highest-yield change available without an architectural migration, and ",[30,31,32],"code",{},"IntersectionObserver"," is the mechanism.",[15,35,36],{},"The rule that makes it safe: defer the JavaScript, never the markup.",[38,39,41],"h2",{"id":40},"rapid-diagnosis-find-what-is-hydrating-off-screen","Rapid Diagnosis: Find What Is Hydrating Off-Screen",[43,44,45,53,59,65],"ul",{},[46,47,48,52],"li",{},[49,50,51],"strong",{},"Record a throttled load and look at what runs after paint."," Components whose names you recognise as below-the-fold widgets are candidates.",[46,54,55,58],{},[49,56,57],{},"Measure their share of the hydration task."," If the carousel and comments together account for 40% of it, deferring them is a 40% cut.",[46,60,61,64],{},[49,62,63],{},"Check what they cost beyond hydration."," Many below-the-fold widgets also fetch data or register observers on mount, which is often larger than their hydration cost.",[46,66,67,70],{},[49,68,69],{},"Confirm the server renders them."," If a widget renders nothing server-side, deferring hydration will not help — the fix there is to server-render it or to remove it from the initial HTML entirely.",[72,73,80,81,88,89,88,93,88,97,88,106,88,113,88,122,88,129,88,134,88,141,88,145,88,150,88,154,88,157,88,161,88,167,88,178,88,182,88,185,88,188,88,190,88,193,88,196,88,199,88,201,88],"svg",{"xmlns":74,"viewBox":75,"width":76,"role":77,"ariaLabel":78,"style":79},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 280","100%","img","Diagram of a page with the viewport at the top, a root margin band of two hundred pixels below it, and three deferred components that hydrate as the band reaches each one during scroll","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)","\n  ",[82,83],"rect",{"className":84,"x":86,"y":86,"width":76,"height":76,"fill":87},[85],"svg-canvas","0","#ffffff"," ",[90,91,92],"title",{},"The root-margin band that triggers hydration ahead of arrival",[94,95,96],"desc",{},"The visible viewport sits at the top of the document, with a two hundred pixel pre-load band beneath it; each deferred component hydrates when that band reaches it, so it is interactive before it scrolls into view.",[82,98],{"x":99,"y":99,"width":100,"height":101,"rx":102,"fill":103,"stroke":104,"style":105},"1","758","278","10","none","currentColor","stroke-opacity:0.18",[107,108,112],"text",{"x":109,"y":110,"fill":104,"style":111},"24","32","font-size:16px;font-weight:700","Hydrate on approach, not on arrival",[82,114],{"x":115,"y":116,"width":117,"height":118,"rx":119,"fill":120,"stroke":120,"style":121},"60","52","240","70","6","#0466c8","fill-opacity:0.18;stroke-opacity:0.55",[107,123,128],{"x":124,"y":125,"fill":104,"style":126,"textAnchor":127},"180","82","font-size:12px;font-weight:700;","middle","viewport",[107,130,133],{"x":124,"y":131,"fill":104,"style":132,"textAnchor":127},"102","font-size:11px;","hydrated eagerly",[82,135],{"x":115,"y":136,"width":117,"height":137,"rx":119,"fill":138,"stroke":139,"style":140},"122","34","#ffc300","#b8860b","stroke-dasharray:5 4;fill-opacity:0.2",[107,142,144],{"x":124,"y":143,"fill":104,"style":132,"textAnchor":127},"144","rootMargin 200px",[82,146],{"x":115,"y":147,"width":117,"height":148,"rx":119,"fill":104,"stroke":104,"style":149},"164","44","fill-opacity:0.05;stroke-opacity:0.3",[107,151,153],{"x":124,"y":152,"fill":104,"style":132,"textAnchor":127},"191","carousel — server HTML, not yet hydrated",[82,155],{"x":115,"y":156,"width":117,"height":148,"rx":119,"fill":104,"stroke":104,"style":149},"214",[107,158,160],{"x":124,"y":159,"fill":104,"style":132,"textAnchor":127},"241","comments — server HTML, not yet hydrated",[107,162,166],{"x":163,"y":164,"fill":104,"style":165,"textAnchor":127},"370","80","font-size:11px;font-weight:700;","Scroll 200px",[168,169],"line",{"x1":170,"y1":171,"x2":172,"y2":173,"stroke":104,"strokeDashArray":174,"style":177},"360","96","420","150",[175,176],"4","3","stroke-opacity:0.45",[82,179],{"x":180,"y":116,"width":181,"height":118,"rx":119,"fill":120,"stroke":120,"style":121},"440","260",[107,183,128],{"x":184,"y":125,"fill":104,"style":126,"textAnchor":127},"570",[107,186,187],{"x":184,"y":131,"fill":104,"style":132,"textAnchor":127},"still responsive",[82,189],{"x":180,"y":136,"width":181,"height":137,"rx":119,"fill":138,"stroke":139,"style":140},[107,191,192],{"x":184,"y":143,"fill":104,"style":132,"textAnchor":127},"band now overlaps the carousel",[82,194],{"x":180,"y":147,"width":181,"height":148,"rx":119,"fill":120,"stroke":120,"style":195},"fill-opacity:0.2;stroke-opacity:0.55",[107,197,198],{"x":184,"y":152,"fill":104,"style":165,"textAnchor":127},"carousel — hydrating now",[82,200],{"x":180,"y":156,"width":181,"height":148,"rx":119,"fill":104,"stroke":104,"style":149},[107,202,203],{"x":184,"y":159,"fill":104,"style":132,"textAnchor":127},"comments — still waiting",[38,205,207],{"id":206},"root-cause-analysis-why-naive-deferral-breaks-things","Root Cause Analysis: Why Naive Deferral Breaks Things",[15,209,210,213],{},[49,211,212],{},"1. Discarding the server markup."," The most common implementation renders an empty placeholder until hydration, which collapses the component's box, produces a layout shift, and removes content from the initial HTML. It converts an INP win into a CLS loss.",[15,215,216,219],{},[49,217,218],{},"2. Keyboard and find-in-page bypass scrolling."," A user tabbing through the document, or using find-in-page, can reach a deferred component without any intersection ever firing. Without a focus-based trigger the control is inert exactly when someone tries to use it.",[15,221,222,225,226,229],{},[49,223,224],{},"3. A root margin that is too small."," With ",[30,227,228],{},"rootMargin: 0px",", hydration begins as the component enters view, so the user sees a moment of unresponsiveness. A band of one to two hundred pixels makes the work finish before arrival.",[15,231,232,235],{},[49,233,234],{},"4. Observers that never disconnect."," Leaving an observer attached after hydration leaks a callback per component and keeps them firing on every scroll — a small cost that becomes visible on long pages with many deferred blocks.",[38,237,239],{"id":238},"step-by-step-resolution","Step-by-Step Resolution",[15,241,242,245],{},[49,243,244],{},"1. Build a boundary that preserves the server HTML."," The key is to keep the existing DOM and only attach behaviour later.",[247,248,253],"pre",{"className":249,"code":250,"language":251,"meta":252,"style":252},"language-jsx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F React: keep server markup, attach interactivity on approach.\nfunction LazyHydrate({ children, margin = '200px' }) {\n  const ref = useRef(null);\n  const [hydrated, setHydrated] = useState(() => typeof window === 'undefined');\n\n  useEffect(() => {\n    if (hydrated) return;\n    const el = ref.current;\n    if (!el) return;\n    const io = new IntersectionObserver(([entry]) => {\n      if (entry.isIntersecting) { setHydrated(true); io.disconnect(); }\n    }, { rootMargin: margin });\n    io.observe(el);\n    return () => io.disconnect();          \u002F\u002F no leaked observers\n  }, [hydrated, margin]);\n\n  if (hydrated) return \u003Cdiv ref={ref}>{children}\u003C\u002Fdiv>;\n  \u002F\u002F Not yet hydrated: keep whatever the server painted, untouched.\n  return \u003Cdiv ref={ref} suppressHydrationWarning\n              dangerouslySetInnerHTML={{ __html: ref.current?.innerHTML ?? '' }} \u002F>;\n}\n\u002F\u002F trade-off: reading innerHTML at render time is a hydration-specific hack that\n\u002F\u002F depends on the server output already being in the DOM. Frameworks with a\n\u002F\u002F built-in lazy-hydration primitive (Nuxt, Astro, Qwik) express this natively —\n\u002F\u002F prefer theirs when you have one.\n","jsx","",[30,254,255,263,298,322,367,374,387,402,416,434,463,488,494,506,528,534,539,585,591,612,637,643,649,655,661],{"__ignoreMap":252},[256,257,259],"span",{"class":168,"line":258},1,[256,260,262],{"class":261},"sjfSM","\u002F\u002F React: keep server markup, attach interactivity on approach.\n",[256,264,266,270,274,278,282,285,288,291,295],{"class":168,"line":265},2,[256,267,269],{"class":268},"sPARh","function",[256,271,273],{"class":272},"smZ65"," LazyHydrate",[256,275,277],{"class":276},"saISM","({ ",[256,279,281],{"class":280},"sQw3B","children",[256,283,284],{"class":276},", ",[256,286,287],{"class":280},"margin",[256,289,290],{"class":268}," =",[256,292,294],{"class":293},"sZ8jY"," '200px'",[256,296,297],{"class":276}," }) {\n",[256,299,301,304,308,310,313,316,319],{"class":168,"line":300},3,[256,302,303],{"class":268},"  const",[256,305,307],{"class":306},"sPXB4"," ref",[256,309,290],{"class":268},[256,311,312],{"class":272}," useRef",[256,314,315],{"class":276},"(",[256,317,318],{"class":306},"null",[256,320,321],{"class":276},");\n",[256,323,325,327,330,333,335,338,341,344,347,350,353,356,359,362,365],{"class":168,"line":324},4,[256,326,303],{"class":268},[256,328,329],{"class":276}," [",[256,331,332],{"class":306},"hydrated",[256,334,284],{"class":276},[256,336,337],{"class":306},"setHydrated",[256,339,340],{"class":276},"] ",[256,342,343],{"class":268},"=",[256,345,346],{"class":272}," useState",[256,348,349],{"class":276},"(() ",[256,351,352],{"class":268},"=>",[256,354,355],{"class":268}," typeof",[256,357,358],{"class":276}," window ",[256,360,361],{"class":268},"===",[256,363,364],{"class":293}," 'undefined'",[256,366,321],{"class":276},[256,368,370],{"class":168,"line":369},5,[256,371,373],{"emptyLinePlaceholder":372},true,"\n",[256,375,377,380,382,384],{"class":168,"line":376},6,[256,378,379],{"class":272},"  useEffect",[256,381,349],{"class":276},[256,383,352],{"class":268},[256,385,386],{"class":276}," {\n",[256,388,390,393,396,399],{"class":168,"line":389},7,[256,391,392],{"class":268},"    if",[256,394,395],{"class":276}," (hydrated) ",[256,397,398],{"class":268},"return",[256,400,401],{"class":276},";\n",[256,403,405,408,411,413],{"class":168,"line":404},8,[256,406,407],{"class":268},"    const",[256,409,410],{"class":306}," el",[256,412,290],{"class":268},[256,414,415],{"class":276}," ref.current;\n",[256,417,419,421,424,427,430,432],{"class":168,"line":418},9,[256,420,392],{"class":268},[256,422,423],{"class":276}," (",[256,425,426],{"class":268},"!",[256,428,429],{"class":276},"el) ",[256,431,398],{"class":268},[256,433,401],{"class":276},[256,435,437,439,442,444,447,450,453,456,459,461],{"class":168,"line":436},10,[256,438,407],{"class":268},[256,440,441],{"class":306}," io",[256,443,290],{"class":268},[256,445,446],{"class":268}," new",[256,448,449],{"class":272}," IntersectionObserver",[256,451,452],{"class":276},"(([",[256,454,455],{"class":280},"entry",[256,457,458],{"class":276},"]) ",[256,460,352],{"class":268},[256,462,386],{"class":276},[256,464,466,469,472,474,476,479,482,485],{"class":168,"line":465},11,[256,467,468],{"class":268},"      if",[256,470,471],{"class":276}," (entry.isIntersecting) { ",[256,473,337],{"class":272},[256,475,315],{"class":276},[256,477,478],{"class":306},"true",[256,480,481],{"class":276},"); io.",[256,483,484],{"class":272},"disconnect",[256,486,487],{"class":276},"(); }\n",[256,489,491],{"class":168,"line":490},12,[256,492,493],{"class":276},"    }, { rootMargin: margin });\n",[256,495,497,500,503],{"class":168,"line":496},13,[256,498,499],{"class":276},"    io.",[256,501,502],{"class":272},"observe",[256,504,505],{"class":276},"(el);\n",[256,507,509,512,515,517,520,522,525],{"class":168,"line":508},14,[256,510,511],{"class":268},"    return",[256,513,514],{"class":276}," () ",[256,516,352],{"class":268},[256,518,519],{"class":276}," io.",[256,521,484],{"class":272},[256,523,524],{"class":276},"();          ",[256,526,527],{"class":261},"\u002F\u002F no leaked observers\n",[256,529,531],{"class":168,"line":530},15,[256,532,533],{"class":276},"  }, [hydrated, margin]);\n",[256,535,537],{"class":168,"line":536},16,[256,538,373],{"emptyLinePlaceholder":372},[256,540,542,545,547,549,552,556,558,561,564,567,570,573,575,577,580,582],{"class":168,"line":541},17,[256,543,544],{"class":268},"  if",[256,546,395],{"class":276},[256,548,398],{"class":268},[256,550,551],{"class":276}," \u003C",[256,553,555],{"class":554},"sZBmE","div",[256,557,307],{"class":306},[256,559,560],{"class":268},"={",[256,562,563],{"class":276},"ref",[256,565,566],{"class":268},"}",[256,568,569],{"class":276},">",[256,571,572],{"class":268},"{",[256,574,281],{"class":276},[256,576,566],{"class":268},[256,578,579],{"class":276},"\u003C\u002F",[256,581,555],{"class":554},[256,583,584],{"class":276},">;\n",[256,586,588],{"class":168,"line":587},18,[256,589,590],{"class":261},"  \u002F\u002F Not yet hydrated: keep whatever the server painted, untouched.\n",[256,592,594,597,599,601,603,605,607,609],{"class":168,"line":593},19,[256,595,596],{"class":268},"  return",[256,598,551],{"class":276},[256,600,555],{"class":554},[256,602,307],{"class":306},[256,604,560],{"class":268},[256,606,563],{"class":276},[256,608,566],{"class":268},[256,610,611],{"class":306}," suppressHydrationWarning\n",[256,613,615,618,620,623,626,629,632,634],{"class":168,"line":614},20,[256,616,617],{"class":306},"              dangerouslySetInnerHTML",[256,619,560],{"class":268},[256,621,622],{"class":276},"{ __html: ref.current?.innerHTML ",[256,624,625],{"class":268},"??",[256,627,628],{"class":293}," ''",[256,630,631],{"class":276}," }",[256,633,566],{"class":268},[256,635,636],{"class":276}," \u002F>;\n",[256,638,640],{"class":168,"line":639},21,[256,641,642],{"class":276},"}\n",[256,644,646],{"class":168,"line":645},22,[256,647,648],{"class":261},"\u002F\u002F trade-off: reading innerHTML at render time is a hydration-specific hack that\n",[256,650,652],{"class":168,"line":651},23,[256,653,654],{"class":261},"\u002F\u002F depends on the server output already being in the DOM. Frameworks with a\n",[256,656,658],{"class":168,"line":657},24,[256,659,660],{"class":261},"\u002F\u002F built-in lazy-hydration primitive (Nuxt, Astro, Qwik) express this natively —\n",[256,662,664],{"class":168,"line":663},25,[256,665,666],{"class":261},"\u002F\u002F prefer theirs when you have one.\n",[15,668,669],{},"Expected outcome: the component's markup and box are unchanged; only its JavaScript execution moves later.",[15,671,672,675],{},[49,673,674],{},"2. Add the escape hatches."," Intersection is not the only way a user reaches a component.",[247,677,681],{"className":678,"code":679,"language":680,"meta":252,"style":252},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Force hydration for keyboard users, find-in-page, print and hash links.\nfunction armFallbacks(el, hydrate) {\n  el.addEventListener('focusin', hydrate, { once: true });\n  addEventListener('beforeprint', hydrate, { once: true });\n  if (location.hash && el.querySelector(location.hash)) hydrate();\n}\n\u002F\u002F trade-off: focusin only fires for focusable descendants. A component whose\n\u002F\u002F only interactive affordance is a click target with no tabindex stays inert —\n\u002F\u002F which is an accessibility bug in the component, not in the deferral.\n","javascript",[30,682,683,688,708,729,745,769,773,778,783],{"__ignoreMap":252},[256,684,685],{"class":168,"line":258},[256,686,687],{"class":261},"\u002F\u002F Force hydration for keyboard users, find-in-page, print and hash links.\n",[256,689,690,692,695,697,700,702,705],{"class":168,"line":265},[256,691,269],{"class":268},[256,693,694],{"class":272}," armFallbacks",[256,696,315],{"class":276},[256,698,699],{"class":280},"el",[256,701,284],{"class":276},[256,703,704],{"class":280},"hydrate",[256,706,707],{"class":276},") {\n",[256,709,710,713,716,718,721,724,726],{"class":168,"line":300},[256,711,712],{"class":276},"  el.",[256,714,715],{"class":272},"addEventListener",[256,717,315],{"class":276},[256,719,720],{"class":293},"'focusin'",[256,722,723],{"class":276},", hydrate, { once: ",[256,725,478],{"class":306},[256,727,728],{"class":276}," });\n",[256,730,731,734,736,739,741,743],{"class":168,"line":324},[256,732,733],{"class":272},"  addEventListener",[256,735,315],{"class":276},[256,737,738],{"class":293},"'beforeprint'",[256,740,723],{"class":276},[256,742,478],{"class":306},[256,744,728],{"class":276},[256,746,747,749,752,755,758,761,764,766],{"class":168,"line":369},[256,748,544],{"class":268},[256,750,751],{"class":276}," (location.hash ",[256,753,754],{"class":268},"&&",[256,756,757],{"class":276}," el.",[256,759,760],{"class":272},"querySelector",[256,762,763],{"class":276},"(location.hash)) ",[256,765,704],{"class":272},[256,767,768],{"class":276},"();\n",[256,770,771],{"class":168,"line":376},[256,772,642],{"class":276},[256,774,775],{"class":168,"line":389},[256,776,777],{"class":261},"\u002F\u002F trade-off: focusin only fires for focusable descendants. A component whose\n",[256,779,780],{"class":168,"line":404},[256,781,782],{"class":261},"\u002F\u002F only interactive affordance is a click target with no tabindex stays inert —\n",[256,784,785],{"class":168,"line":418},[256,786,787],{"class":261},"\u002F\u002F which is an accessibility bug in the component, not in the deferral.\n",[15,789,790],{},"Expected outcome: no path to the component leaves it non-functional.",[15,792,793,796,797,800],{},[49,794,795],{},"3. Tune the margin against real scroll speed."," Start at ",[30,798,799],{},"200px"," and check on a mid-tier device: if a fast flick reaches the component before hydration completes, raise it; if you find components hydrating that the user never reaches, lower it. A useful heuristic is roughly one third of the viewport height.",[15,802,803,806],{},[49,804,805],{},"4. Order the work so it cannot pile up."," If three deferred components enter the band at once — common on a fast scroll — hydrating all three in one frame recreates the long task you were avoiding.",[247,808,810],{"className":678,"code":809,"language":680,"meta":252,"style":252},"\u002F\u002F Hydrate at most one component per frame, yielding between them.\nconst pending = [];\nlet scheduled = false;\nfunction enqueue(fn) {\n  pending.push(fn);\n  if (scheduled) return;\n  scheduled = true;\n  const run = () => {\n    const next = pending.shift();\n    if (next) next();\n    if (pending.length) requestAnimationFrame(run); else scheduled = false;\n  };\n  requestAnimationFrame(run);\n}\n\u002F\u002F trade-off: serialising hydration adds a frame of latency per component when\n\u002F\u002F several arrive together. That is the right trade — three 30ms tasks with\n\u002F\u002F gaps beat one 90ms task that blocks a tap.\n",[30,811,812,817,830,845,859,870,881,893,908,925,937,967,972,980,984,989,994],{"__ignoreMap":252},[256,813,814],{"class":168,"line":258},[256,815,816],{"class":261},"\u002F\u002F Hydrate at most one component per frame, yielding between them.\n",[256,818,819,822,825,827],{"class":168,"line":265},[256,820,821],{"class":268},"const",[256,823,824],{"class":306}," pending",[256,826,290],{"class":268},[256,828,829],{"class":276}," [];\n",[256,831,832,835,838,840,843],{"class":168,"line":300},[256,833,834],{"class":268},"let",[256,836,837],{"class":276}," scheduled ",[256,839,343],{"class":268},[256,841,842],{"class":306}," false",[256,844,401],{"class":276},[256,846,847,849,852,854,857],{"class":168,"line":324},[256,848,269],{"class":268},[256,850,851],{"class":272}," enqueue",[256,853,315],{"class":276},[256,855,856],{"class":280},"fn",[256,858,707],{"class":276},[256,860,861,864,867],{"class":168,"line":369},[256,862,863],{"class":276},"  pending.",[256,865,866],{"class":272},"push",[256,868,869],{"class":276},"(fn);\n",[256,871,872,874,877,879],{"class":168,"line":376},[256,873,544],{"class":268},[256,875,876],{"class":276}," (scheduled) ",[256,878,398],{"class":268},[256,880,401],{"class":276},[256,882,883,886,888,891],{"class":168,"line":389},[256,884,885],{"class":276},"  scheduled ",[256,887,343],{"class":268},[256,889,890],{"class":306}," true",[256,892,401],{"class":276},[256,894,895,897,900,902,904,906],{"class":168,"line":404},[256,896,303],{"class":268},[256,898,899],{"class":272}," run",[256,901,290],{"class":268},[256,903,514],{"class":276},[256,905,352],{"class":268},[256,907,386],{"class":276},[256,909,910,912,915,917,920,923],{"class":168,"line":418},[256,911,407],{"class":268},[256,913,914],{"class":306}," next",[256,916,290],{"class":268},[256,918,919],{"class":276}," pending.",[256,921,922],{"class":272},"shift",[256,924,768],{"class":276},[256,926,927,929,932,935],{"class":168,"line":436},[256,928,392],{"class":268},[256,930,931],{"class":276}," (next) ",[256,933,934],{"class":272},"next",[256,936,768],{"class":276},[256,938,939,941,944,947,950,953,956,959,961,963,965],{"class":168,"line":465},[256,940,392],{"class":268},[256,942,943],{"class":276}," (pending.",[256,945,946],{"class":306},"length",[256,948,949],{"class":276},") ",[256,951,952],{"class":272},"requestAnimationFrame",[256,954,955],{"class":276},"(run); ",[256,957,958],{"class":268},"else",[256,960,837],{"class":276},[256,962,343],{"class":268},[256,964,842],{"class":306},[256,966,401],{"class":276},[256,968,969],{"class":168,"line":490},[256,970,971],{"class":276},"  };\n",[256,973,974,977],{"class":168,"line":496},[256,975,976],{"class":272},"  requestAnimationFrame",[256,978,979],{"class":276},"(run);\n",[256,981,982],{"class":168,"line":508},[256,983,642],{"class":276},[256,985,986],{"class":168,"line":530},[256,987,988],{"class":261},"\u002F\u002F trade-off: serialising hydration adds a frame of latency per component when\n",[256,990,991],{"class":168,"line":536},[256,992,993],{"class":261},"\u002F\u002F several arrive together. That is the right trade — three 30ms tasks with\n",[256,995,996],{"class":168,"line":541},[256,997,998],{"class":261},"\u002F\u002F gaps beat one 90ms task that blocks a tap.\n",[15,1000,1001],{},"Expected outcome: deferred hydration never reconstructs a long task, even during fast scrolling.",[38,1003,1005],{"id":1004},"verification","Verification",[15,1007,1008],{},"Confirm the three things that can go wrong, in order. First, layout stability: scroll slowly past each boundary with the Layout Shift Regions overlay on — nothing should highlight. Second, interactivity: tab through the whole document with JavaScript enabled and confirm every control responds, then repeat using find-in-page to jump directly to a deferred section. Third, the actual saving: re-record the throttled load and compare the hydration task.",[247,1010,1012],{"className":678,"code":1011,"language":680,"meta":252,"style":252},"\u002F\u002F A quick check that deferral is really happening.\nperformance.getEntriesByType('longtask')\n  .filter((t) => t.duration > 50)\n  .forEach((t) => console.log('long task', Math.round(t.duration), 'at',\n    Math.round(t.startTime)));\n\u002F\u002F Expect: no long tasks in the window between first paint and first interaction.\n",[30,1013,1014,1019,1035,1063,1104,1114],{"__ignoreMap":252},[256,1015,1016],{"class":168,"line":258},[256,1017,1018],{"class":261},"\u002F\u002F A quick check that deferral is really happening.\n",[256,1020,1021,1024,1027,1029,1032],{"class":168,"line":265},[256,1022,1023],{"class":276},"performance.",[256,1025,1026],{"class":272},"getEntriesByType",[256,1028,315],{"class":276},[256,1030,1031],{"class":293},"'longtask'",[256,1033,1034],{"class":276},")\n",[256,1036,1037,1040,1043,1046,1049,1051,1053,1056,1058,1061],{"class":168,"line":300},[256,1038,1039],{"class":276},"  .",[256,1041,1042],{"class":272},"filter",[256,1044,1045],{"class":276},"((",[256,1047,1048],{"class":280},"t",[256,1050,949],{"class":276},[256,1052,352],{"class":268},[256,1054,1055],{"class":276}," t.duration ",[256,1057,569],{"class":268},[256,1059,1060],{"class":306}," 50",[256,1062,1034],{"class":276},[256,1064,1065,1067,1070,1072,1074,1076,1078,1081,1084,1086,1089,1092,1095,1098,1101],{"class":168,"line":324},[256,1066,1039],{"class":276},[256,1068,1069],{"class":272},"forEach",[256,1071,1045],{"class":276},[256,1073,1048],{"class":280},[256,1075,949],{"class":276},[256,1077,352],{"class":268},[256,1079,1080],{"class":276}," console.",[256,1082,1083],{"class":272},"log",[256,1085,315],{"class":276},[256,1087,1088],{"class":293},"'long task'",[256,1090,1091],{"class":276},", Math.",[256,1093,1094],{"class":272},"round",[256,1096,1097],{"class":276},"(t.duration), ",[256,1099,1100],{"class":293},"'at'",[256,1102,1103],{"class":276},",\n",[256,1105,1106,1109,1111],{"class":168,"line":369},[256,1107,1108],{"class":276},"    Math.",[256,1110,1094],{"class":272},[256,1112,1113],{"class":276},"(t.startTime)));\n",[256,1115,1116],{"class":168,"line":376},[256,1117,1118],{"class":261},"\u002F\u002F Expect: no long tasks in the window between first paint and first interaction.\n",[15,1120,1121],{},"In the field, watch INP for early interactions, and watch CLS to confirm the deferral did not introduce shifts. Both should move in the right direction; if CLS worsens, a placeholder is collapsing somewhere and the markup is not being preserved.",[38,1123,1125],{"id":1124},"faq","FAQ",[1127,1128,1131,1135],"details",{"className":1129},[1130],"faq-item",[1132,1133,1134],"summary",{},"Does deferring hydration hurt SEO?",[15,1136,1137],{},"No, provided the server markup stays in the document. Crawlers read the rendered HTML, which is complete before any hydration decision is made. The pattern only becomes an SEO problem if the placeholder replaces server content with an empty box — which is also the version that causes layout shift, so the same rule fixes both.",[1127,1139,1141,1144],{"className":1140},[1130],[1132,1142,1143],{},"What rootMargin should I use?",[15,1145,1146,1147,1149],{},"Start at ",[30,1148,799],{}," and tune with a real device. The goal is that hydration finishes before the component is visible, so the right value depends on scroll velocity and on how long the component takes to hydrate. Roughly a third of the viewport height is a good starting heuristic; anything above a full viewport usually means you are hydrating things the user will never see.",[1127,1151,1153,1156],{"className":1152},[1130],[1132,1154,1155],{},"Is this better than just code-splitting the component?",[15,1157,1158,1159,1163,1164,1167],{},"They solve different halves. Code splitting defers the ",[1160,1161,1162],"em",{},"download and parse"," of the chunk; lazy hydration defers the ",[1160,1165,1166],{},"execution"," that instantiates the component. Combining them is the strongest option: the chunk loads when the component approaches, and hydration runs immediately after. Splitting alone still leaves the tree walk in the initial hydration task if the component is part of it.",[1127,1169,1171,1174],{"className":1170},[1130],[1132,1172,1173],{},"Which components are the best candidates to defer?",[15,1175,1176,1177,1180],{},"Rank by hydration cost multiplied by the probability the user never reaches them. Comment threads, related-content carousels, footers with interactive widgets and newsletter forms score highly on both axes. Anything above the fold scores zero on the second and should stay eager regardless of cost. If a component is expensive ",[1160,1178,1179],{},"and"," frequently used, deferral only moves the cost — the answer there is to make it cheaper.",[1127,1182,1184,1187],{"className":1183},[1130],[1132,1185,1186],{},"Does this work with server components or islands frameworks?",[15,1188,1189],{},"Those architectures already provide it, usually more cleanly. An islands framework's visibility directive is this pattern implemented at the framework level, with the markup preservation handled for you. The hand-rolled boundary here is for applications that hydrate the whole tree and are not migrating — if your framework offers a lazy-hydration primitive, use it rather than reimplementing the mechanics.",[1127,1191,1193,1196],{"className":1192},[1130],[1132,1194,1195],{},"How do I test that a deferred component still works?",[15,1197,1198],{},"Exercise all three entry paths in an end-to-end test: scroll to it, tab to it, and jump to it via a hash link. A test that only scrolls will pass while keyboard users find an inert control. It is also worth asserting that the component's markup is present in the server response before any hydration happens, which catches the regression where someone replaces the preserved HTML with an empty placeholder.",[38,1200,1202],{"id":1201},"related","Related",[43,1204,1205,1210,1217],{},[46,1206,1207,1209],{},[19,1208,22],{"href":21}," — where this pattern fits among the alternatives.",[46,1211,1212,1216],{},[19,1213,1215],{"href":1214},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fislands-vs-full-hydration-for-content-sites\u002F","Islands vs full hydration for content sites"," — when to stop deferring and change architecture instead.",[46,1218,1219,1223],{},[19,1220,1222],{"href":1221},"\u002Fimage-media-optimization\u002Flazy-loading-images-without-hurting-lcp\u002Fnative-lazy-loading-vs-intersection-observer\u002F","Native lazy loading vs IntersectionObserver"," — the same observer applied to images, with different trade-offs.",[1225,1226,1228],"script",{"type":1227},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Defer below-the-fold hydration with IntersectionObserver\",\n  \"description\": \"Build a lazy-hydration boundary that preserves server markup, hydrates components as they approach the viewport, provides keyboard and print fallbacks, and serialises concurrent hydration.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"position\": 1, \"name\": \"Preserve the server markup\", \"text\": \"Create a boundary that keeps the server-rendered HTML and attaches behaviour later rather than rendering an empty placeholder.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 2, \"name\": \"Add escape hatches\", \"text\": \"Trigger hydration on focusin, before printing, and for hash links so keyboard and find-in-page users are not left with inert controls.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 3, \"name\": \"Tune the root margin\", \"text\": \"Set the pre-load band so hydration completes before the component becomes visible at realistic scroll speeds.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 4, \"name\": \"Serialise concurrent hydration\", \"text\": \"Queue hydration one component per frame so several components entering the band together cannot form a long task.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002F#step-by-step-resolution\" }\n  ]\n}\n",[1225,1230,1231],{"type":1227},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Defer Below-the-Fold Hydration with IntersectionObserver\",\n  \"description\": \"A lazy-hydration boundary that preserves server HTML, avoids layout shift, keeps keyboard access working and never rebuilds a long task.\",\n  \"datePublished\": \"2026-08-01\",\n  \"dateModified\": \"2026-08-01\",\n  \"author\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"publisher\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"mainEntityOfPage\": { \"@type\": \"WebPage\", \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002F\" }\n}\n",[1225,1233,1234],{"type":1227},"\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\": \"JavaScript Bundle Optimization & Code Splitting\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"Hydration & Partial Rehydration Strategies\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Deferring Below-the-Fold Hydration with IntersectionObserver\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002F\" }\n  ]\n}\n",[1236,1237,1238],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}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 .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--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":252,"searchDepth":265,"depth":265,"links":1240},[1241,1242,1243,1244,1245,1246],{"id":40,"depth":265,"text":41},{"id":206,"depth":265,"text":207},{"id":238,"depth":265,"text":239},{"id":1004,"depth":265,"text":1005},{"id":1124,"depth":265,"text":1125},{"id":1201,"depth":265,"text":1202},"A framework-agnostic lazy-hydration boundary that keeps server markup intact, hydrates on approach, and still works for keyboard, find-in-page and print.","md",{"slug":1250,"type":1251,"breadcrumb":1252,"datePublished":1260,"dateModified":1260},"deferring-below-the-fold-hydration-with-intersectionobserver","article",[1253,1256,1257,1258],{"name":1254,"url":1255},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1259},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002F","2026-08-01","\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver",{"title":5,"description":1263},"Hydrate off-screen components only when they approach the viewport: preserve the server HTML, avoid layout shift, keep keyboard access, and handle print and search.","javascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002Findex","NjmH-Cj0EOpLFVn0NpGi2YS13_wok_fIty9nG-JTx9s",[1267,1270],{"title":22,"path":1268,"stem":1269,"children":-1},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies","javascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Findex",{"title":1271,"path":1272,"stem":1273,"children":-1},"Fixing Long Hydration Tasks in Next.js App Router","\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router","javascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router\u002Findex",1785622406676]