[{"data":1,"prerenderedAt":839},["ShallowReactive",2],{"content:\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router":3,"surroundings:\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router":830},{"id":4,"title":5,"body":6,"description":810,"extension":811,"meta":812,"navigation":504,"path":824,"seo":825,"stem":828,"__hash__":829},"content\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router\u002Findex.md","Fixing Long Hydration Tasks in Next.js App Router",{"type":7,"value":8,"toc":802},"minimark",[9,14,42,47,96,257,261,273,283,289,299,303,309,397,408,414,473,476,482,571,574,584,619,622,626,633,636,679,682,686,702,715,727,736,749,762,766,787,792,795,798],[10,11,13],"h1",{"id":12},"how-to-fix-a-500ms-hydration-task-in-the-nextjs-app-router","How to Fix a 500ms Hydration Task in the Next.js App Router",[15,16,17,18,23,24,28,29,33,34,37,38,41],"p",{},"This walkthrough sits under ",[19,20,22],"a",{"href":21},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002F","Hydration & Partial Rehydration Strategies",", part of ",[19,25,27],{"href":26},"\u002Fjavascript-bundle-optimization-code-splitting\u002F","JavaScript Bundle Optimization & Code Splitting",". The App Router's promise is that server components ship no JavaScript, so hydration should be cheap. In practice one ",[30,31,32],"code",{},"'use client'"," directive placed too high converts the entire subtree beneath it into client components, and you are back to hydrating the whole page — with a ",[30,35,36],{},"500–900ms"," task on a throttled profile and an INP that fails the ",[30,39,40],{},"200ms"," threshold.",[43,44,46],"h2",{"id":45},"rapid-diagnosis-find-the-boundary-that-swallowed-the-tree","Rapid Diagnosis: Find the Boundary That Swallowed the Tree",[48,49,50,62,71,81],"ul",{},[51,52,53,57,58,61],"li",{},[54,55,56],"strong",{},"Look at the long task, not the bundle size."," Record a throttled load; the hydration task appears as one long block shortly after first paint, containing ",[30,59,60],{},"hydrateRoot"," in its stack.",[51,63,64,67,68,70],{},[54,65,66],{},"Count client components."," Any module with ",[30,69,32],{}," at the top, plus everything it imports, is a client component. A layout or a provider wrapper with the directive is the usual offender because everything renders inside it.",[51,72,73,76,77,80],{},[54,74,75],{},"Read the RSC payload size."," View source and find the inline ",[30,78,79],{},"self.__next_f.push"," flight data. Over ~100KB is a signal that large props or serialised data are travelling to the client.",[51,82,83,86,87,90,91,95],{},[54,84,85],{},"Check for a context provider at the root."," Theme, auth, i18n and analytics providers are frequently client components wrapping ",[30,88,89],{},"{children}",", which is exactly the pattern that keeps the subtree on the server — unless the provider also ",[92,93,94],"em",{},"renders"," the children through client code.",[97,98,105,106,113,114,113,118,113,122,113,131,113,138,113,145,113,155,113,160,113,164,113,170,113,172,113,176,113,179,113,183,113,186,113,190,113,192,113,195,113,197,113,200,113,204,113,211,113,215,113,219,113,221,113,224,113,227,113,229,113,232,113,235,113,238,113,242,113,245,113,247,113,249,113,251,113,254,113],"svg",{"xmlns":99,"viewBox":100,"width":101,"role":102,"ariaLabel":103,"style":104},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 268","100%","img","Two component trees: one with use client at the layout level turning every node into a client component, and one with use client only on the two leaf controls that need interactivity","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)","\n  ",[107,108],"rect",{"className":109,"x":111,"y":111,"width":101,"height":101,"fill":112},[110],"svg-canvas","0","#ffffff"," ",[115,116,117],"title",{},"Where the client boundary sits decides what hydrates",[119,120,121],"desc",{},"In the first tree the directive is on the layout so all nine nodes hydrate; in the second it is on two leaves so only those two hydrate and the rest stay server-rendered HTML.",[107,123],{"x":124,"y":124,"width":125,"height":126,"rx":127,"fill":128,"stroke":129,"style":130},"1","758","266","10","none","currentColor","stroke-opacity:0.18",[132,133,137],"text",{"x":134,"y":135,"fill":129,"style":136},"24","32","font-size:16px;font-weight:700","One directive, two very different pages",[132,139,144],{"x":140,"y":141,"fill":129,"style":142,"textAnchor":143},"120","60","font-size:12px;font-weight:700;","middle","'use client' on the layout",[107,146],{"x":147,"y":148,"width":149,"height":150,"rx":151,"fill":152,"stroke":153,"style":154},"70","72","100","28","4","#ffc300","#b8860b","fill-opacity:0.28",[132,156,159],{"x":140,"y":157,"fill":129,"style":158,"textAnchor":143},"91","font-size:11px;","Layout",[107,161],{"x":134,"y":140,"width":162,"height":163,"rx":151,"fill":152,"stroke":153,"style":154},"86","26",[132,165,169],{"x":166,"y":167,"fill":129,"style":168,"textAnchor":143},"67","138","font-size:10.5px;","Header",[107,171],{"x":140,"y":140,"width":162,"height":163,"rx":151,"fill":152,"stroke":153,"style":154},[132,173,175],{"x":174,"y":167,"fill":129,"style":168,"textAnchor":143},"163","Article",[107,177],{"x":178,"y":140,"width":162,"height":163,"rx":151,"fill":152,"stroke":153,"style":154},"216",[132,180,182],{"x":181,"y":167,"fill":129,"style":168,"textAnchor":143},"259","Footer",[107,184],{"x":134,"y":185,"width":162,"height":163,"rx":151,"fill":152,"stroke":153,"style":154},"160",[132,187,189],{"x":166,"y":188,"fill":129,"style":168,"textAnchor":143},"178","Nav",[107,191],{"x":140,"y":185,"width":162,"height":163,"rx":151,"fill":152,"stroke":153,"style":154},[132,193,194],{"x":174,"y":188,"fill":129,"style":168,"textAnchor":143},"Prose",[107,196],{"x":178,"y":185,"width":162,"height":163,"rx":151,"fill":152,"stroke":153,"style":154},[132,198,199],{"x":181,"y":188,"fill":129,"style":168,"textAnchor":143},"Links",[132,201,203],{"x":174,"y":178,"fill":129,"style":202,"textAnchor":143},"font-size:11.5px;font-weight:700;","9 components hydrate — 620ms",[205,206],"line",{"x1":207,"y1":208,"x2":207,"y2":209,"stroke":129,"style":210},"380","50","240","stroke-opacity:0.2",[132,212,214],{"x":213,"y":141,"fill":129,"style":142,"textAnchor":143},"560","'use client' on two leaves",[107,216],{"x":217,"y":148,"width":149,"height":150,"rx":151,"fill":129,"stroke":129,"style":218},"510","fill-opacity:0.05;stroke-opacity:0.3",[132,220,159],{"x":213,"y":157,"fill":129,"style":158,"textAnchor":143},[107,222],{"x":223,"y":140,"width":162,"height":163,"rx":151,"fill":129,"stroke":129,"style":218},"464",[132,225,169],{"x":226,"y":167,"fill":129,"style":168,"textAnchor":143},"507",[107,228],{"x":213,"y":140,"width":162,"height":163,"rx":151,"fill":129,"stroke":129,"style":218},[132,230,175],{"x":231,"y":167,"fill":129,"style":168,"textAnchor":143},"603",[107,233],{"x":234,"y":140,"width":162,"height":163,"rx":151,"fill":129,"stroke":129,"style":218},"656",[132,236,182],{"x":237,"y":167,"fill":129,"style":168,"textAnchor":143},"699",[107,239],{"x":223,"y":185,"width":162,"height":163,"rx":151,"fill":240,"stroke":240,"style":241},"#0466c8","fill-opacity:0.2;stroke-opacity:0.55",[132,243,244],{"x":226,"y":188,"fill":129,"style":168,"textAnchor":143},"Nav menu",[107,246],{"x":213,"y":185,"width":162,"height":163,"rx":151,"fill":129,"stroke":129,"style":218},[132,248,194],{"x":231,"y":188,"fill":129,"style":168,"textAnchor":143},[107,250],{"x":234,"y":185,"width":162,"height":163,"rx":151,"fill":240,"stroke":240,"style":241},[132,252,253],{"x":237,"y":188,"fill":129,"style":168,"textAnchor":143},"Share",[132,255,256],{"x":231,"y":178,"fill":129,"style":202,"textAnchor":143},"2 components hydrate — 40ms",[43,258,260],{"id":259},"root-cause-analysis","Root Cause Analysis",[15,262,263,113,266,268,269,272],{},[54,264,265],{},"1. A client boundary too high in the tree.",[30,267,32],{}," marks a module ",[92,270,271],{},"and everything it imports"," as client code. Placing it on a layout, a page, or a shared wrapper converts the whole route. This is the dominant cause and usually the only one that matters.",[15,274,275,278,279,282],{},[54,276,277],{},"2. Providers that render children through client code."," A provider can be a client component while its children stay server components — but only if the children are passed as the ",[30,280,281],{},"children"," prop from a server parent. Importing and rendering them inside the client module instead pulls them across the boundary.",[15,284,285,288],{},[54,286,287],{},"3. An oversized flight payload."," Every prop crossing from server to client is serialised into the HTML. Passing a fully expanded list of 500 records to a client table means downloading it twice — once as HTML, once as flight data — and parsing the second copy on the main thread before hydration starts.",[15,290,291,294,295,298],{},[54,292,293],{},"4. Barrel imports dragging in client modules."," An ",[30,296,297],{},"index.ts"," that re-exports fifty components means importing one client component from it can pull the rest of the barrel into the client graph, inflating both the bundle and the hydrated tree.",[43,300,302],{"id":301},"step-by-step-resolution","Step-by-Step Resolution",[15,304,305,308],{},[54,306,307],{},"1. Push the directive to the leaves."," Keep layouts and pages as server components; mark only the interactive controls.",[310,311,316],"pre",{"className":312,"code":313,"language":314,"meta":315,"style":315},"language-tsx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F app\u002Flayout.tsx — NO 'use client'\nimport { MobileNavToggle } from '@\u002Fcomponents\u002Fmobile-nav-toggle';   \u002F\u002F client leaf\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003Chtml lang=\"en\">\u003Cbody>\n      \u003Cheader>\u003Cnav>{\u002F* static markup *\u002F}\u003CMobileNavToggle \u002F>\u003C\u002Fnav>\u003C\u002Fheader>\n      {children}\n    \u003C\u002Fbody>\u003C\u002Fhtml>\n  );\n}\n\u002F\u002F trade-off: leaf-level boundaries mean more small client chunks and slightly\n\u002F\u002F more request overhead than one big one. That is a good trade below a few\n\u002F\u002F dozen islands; past that, group related controls into one client module.\n","tsx","",[30,317,318,325,331,337,343,349,355,361,367,373,379,385,391],{"__ignoreMap":315},[319,320,322],"span",{"class":205,"line":321},1,[319,323,324],{},"\u002F\u002F app\u002Flayout.tsx — NO 'use client'\n",[319,326,328],{"class":205,"line":327},2,[319,329,330],{},"import { MobileNavToggle } from '@\u002Fcomponents\u002Fmobile-nav-toggle';   \u002F\u002F client leaf\n",[319,332,334],{"class":205,"line":333},3,[319,335,336],{},"export default function Layout({ children }: { children: React.ReactNode }) {\n",[319,338,340],{"class":205,"line":339},4,[319,341,342],{},"  return (\n",[319,344,346],{"class":205,"line":345},5,[319,347,348],{},"    \u003Chtml lang=\"en\">\u003Cbody>\n",[319,350,352],{"class":205,"line":351},6,[319,353,354],{},"      \u003Cheader>\u003Cnav>{\u002F* static markup *\u002F}\u003CMobileNavToggle \u002F>\u003C\u002Fnav>\u003C\u002Fheader>\n",[319,356,358],{"class":205,"line":357},7,[319,359,360],{},"      {children}\n",[319,362,364],{"class":205,"line":363},8,[319,365,366],{},"    \u003C\u002Fbody>\u003C\u002Fhtml>\n",[319,368,370],{"class":205,"line":369},9,[319,371,372],{},"  );\n",[319,374,376],{"class":205,"line":375},10,[319,377,378],{},"}\n",[319,380,382],{"class":205,"line":381},11,[319,383,384],{},"\u002F\u002F trade-off: leaf-level boundaries mean more small client chunks and slightly\n",[319,386,388],{"class":205,"line":387},12,[319,389,390],{},"\u002F\u002F more request overhead than one big one. That is a good trade below a few\n",[319,392,394],{"class":205,"line":393},13,[319,395,396],{},"\u002F\u002F dozen islands; past that, group related controls into one client module.\n",[15,398,399,400,403,404,407],{},"Expected outcome: the hydrated component count drops by an order of magnitude; the hydration task typically falls from ",[30,401,402],{},"600ms"," to under ",[30,405,406],{},"80ms",".",[15,409,410,413],{},[54,411,412],{},"2. Pass children through, do not import them."," This is the pattern that lets a client provider wrap server content.",[310,415,417],{"className":312,"code":416,"language":314,"meta":315,"style":315},"'use client';\n\u002F\u002F components\u002Ftheme-provider.tsx — a client component that keeps children server-side\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n  const [theme, setTheme] = useState('light');\n  return \u003CThemeCtx.Provider value={{ theme, setTheme }}>{children}\u003C\u002FThemeCtx.Provider>;\n}\n\u002F\u002F Used from a SERVER component:\n\u002F\u002F   \u003CThemeProvider>\u003CServerPage \u002F>\u003C\u002FThemeProvider>\n\u002F\u002F trade-off: server children cannot consume the client context. Anything that\n\u002F\u002F truly needs the value must itself be a client component — keep those small\n\u002F\u002F and push them to the leaves too.\n",[30,418,419,424,429,434,439,444,448,453,458,463,468],{"__ignoreMap":315},[319,420,421],{"class":205,"line":321},[319,422,423],{},"'use client';\n",[319,425,426],{"class":205,"line":327},[319,427,428],{},"\u002F\u002F components\u002Ftheme-provider.tsx — a client component that keeps children server-side\n",[319,430,431],{"class":205,"line":333},[319,432,433],{},"export function ThemeProvider({ children }: { children: React.ReactNode }) {\n",[319,435,436],{"class":205,"line":339},[319,437,438],{},"  const [theme, setTheme] = useState('light');\n",[319,440,441],{"class":205,"line":345},[319,442,443],{},"  return \u003CThemeCtx.Provider value={{ theme, setTheme }}>{children}\u003C\u002FThemeCtx.Provider>;\n",[319,445,446],{"class":205,"line":351},[319,447,378],{},[319,449,450],{"class":205,"line":357},[319,451,452],{},"\u002F\u002F Used from a SERVER component:\n",[319,454,455],{"class":205,"line":363},[319,456,457],{},"\u002F\u002F   \u003CThemeProvider>\u003CServerPage \u002F>\u003C\u002FThemeProvider>\n",[319,459,460],{"class":205,"line":369},[319,461,462],{},"\u002F\u002F trade-off: server children cannot consume the client context. Anything that\n",[319,464,465],{"class":205,"line":375},[319,466,467],{},"\u002F\u002F truly needs the value must itself be a client component — keep those small\n",[319,469,470],{"class":205,"line":381},[319,471,472],{},"\u002F\u002F and push them to the leaves too.\n",[15,474,475],{},"Expected outcome: providers stay interactive while the content they wrap ships zero JavaScript.",[15,477,478,481],{},[54,479,480],{},"3. Shrink what crosses the boundary."," Send the minimum a client component needs, and let it fetch or compute the rest.",[310,483,485],{"className":312,"code":484,"language":314,"meta":315,"style":315},"\u002F\u002F Before: the whole dataset is serialised into the HTML twice.\n\u002F\u002F \u003CDataTable rows={allRows} \u002F>                     \u002F\u002F 380KB of flight data\n\n\u002F\u002F After: render the table as HTML on the server, hydrate only the controls.\nexport default async function Page() {\n  const rows = await getRows();\n  return (\n    \u003C>\n      \u003CTableFilters defaultSort=\"name\" \u002F>           {\u002F* client, tiny props *\u002F}\n      \u003Ctable>{rows.map((r) => \u003CRow key={r.id} {...r} \u002F>)}\u003C\u002Ftable>  {\u002F* server *\u002F}\n    \u003C\u002F>\n  );\n}\n\u002F\u002F trade-off: client-side sorting now needs either a server round trip or a\n\u002F\u002F re-fetch. For tables under a few hundred rows that is usually faster than\n\u002F\u002F shipping and parsing the data twice — measure both on a throttled profile.\n",[30,486,487,492,500,506,511,516,521,525,530,535,540,545,549,553,559,565],{"__ignoreMap":315},[319,488,489],{"class":205,"line":321},[319,490,491],{},"\u002F\u002F Before: the whole dataset is serialised into the HTML twice.\n",[319,493,494,497],{"class":205,"line":327},[319,495,496],{},"\u002F\u002F \u003CDataTable rows={allRows} \u002F>",[319,498,499],{},"                     \u002F\u002F 380KB of flight data\n",[319,501,502],{"class":205,"line":333},[319,503,505],{"emptyLinePlaceholder":504},true,"\n",[319,507,508],{"class":205,"line":339},[319,509,510],{},"\u002F\u002F After: render the table as HTML on the server, hydrate only the controls.\n",[319,512,513],{"class":205,"line":345},[319,514,515],{},"export default async function Page() {\n",[319,517,518],{"class":205,"line":351},[319,519,520],{},"  const rows = await getRows();\n",[319,522,523],{"class":205,"line":357},[319,524,342],{},[319,526,527],{"class":205,"line":363},[319,528,529],{},"    \u003C>\n",[319,531,532],{"class":205,"line":369},[319,533,534],{},"      \u003CTableFilters defaultSort=\"name\" \u002F>           {\u002F* client, tiny props *\u002F}\n",[319,536,537],{"class":205,"line":375},[319,538,539],{},"      \u003Ctable>{rows.map((r) => \u003CRow key={r.id} {...r} \u002F>)}\u003C\u002Ftable>  {\u002F* server *\u002F}\n",[319,541,542],{"class":205,"line":381},[319,543,544],{},"    \u003C\u002F>\n",[319,546,547],{"class":205,"line":387},[319,548,372],{},[319,550,551],{"class":205,"line":393},[319,552,378],{},[319,554,556],{"class":205,"line":555},14,[319,557,558],{},"\u002F\u002F trade-off: client-side sorting now needs either a server round trip or a\n",[319,560,562],{"class":205,"line":561},15,[319,563,564],{},"\u002F\u002F re-fetch. For tables under a few hundred rows that is usually faster than\n",[319,566,568],{"class":205,"line":567},16,[319,569,570],{},"\u002F\u002F shipping and parsing the data twice — measure both on a throttled profile.\n",[15,572,573],{},"Expected outcome: flight payload drops sharply; parse time before hydration falls with it.",[15,575,576,579,580,583],{},[54,577,578],{},"4. Defer the client components that are not immediately needed."," Use ",[30,581,582],{},"next\u002Fdynamic"," for below-the-fold interactive widgets so their chunk and their hydration both wait.",[310,585,587],{"className":312,"code":586,"language":314,"meta":315,"style":315},"const Comments = dynamic(() => import('@\u002Fcomponents\u002Fcomments'), {\n  loading: () => \u003Cdiv style={{ minHeight: 420 }} \u002F>,   \u002F\u002F reserve the box\n});\n\u002F\u002F trade-off: ssr defaults to true here, which keeps the server HTML. Setting\n\u002F\u002F ssr:false removes the markup entirely — faster, but it costs you the content\n\u002F\u002F in the initial HTML and risks a layout shift when it appears.\n",[30,588,589,594,599,604,609,614],{"__ignoreMap":315},[319,590,591],{"class":205,"line":321},[319,592,593],{},"const Comments = dynamic(() => import('@\u002Fcomponents\u002Fcomments'), {\n",[319,595,596],{"class":205,"line":327},[319,597,598],{},"  loading: () => \u003Cdiv style={{ minHeight: 420 }} \u002F>,   \u002F\u002F reserve the box\n",[319,600,601],{"class":205,"line":333},[319,602,603],{},"});\n",[319,605,606],{"class":205,"line":339},[319,607,608],{},"\u002F\u002F trade-off: ssr defaults to true here, which keeps the server HTML. Setting\n",[319,610,611],{"class":205,"line":345},[319,612,613],{},"\u002F\u002F ssr:false removes the markup entirely — faster, but it costs you the content\n",[319,615,616],{"class":205,"line":351},[319,617,618],{},"\u002F\u002F in the initial HTML and risks a layout shift when it appears.\n",[15,620,621],{},"Expected outcome: removes the widget's chunk from the critical path without losing its server-rendered content.",[43,623,625],{"id":624},"verification","Verification",[15,627,628,629,632],{},"Re-record the throttled load and compare the hydration task directly. A successful fix shows no single task above ",[30,630,631],{},"50ms"," in the window between first paint and interactivity, and the flame graph shows short, separated hydration blocks rather than one continuous one.",[15,634,635],{},"Check the build output too — Next.js reports the First Load JS per route, and pushing boundaries down should reduce it measurably:",[310,637,641],{"className":638,"code":639,"language":640,"meta":315,"style":315},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","next build\n# Route (app)                    Size     First Load JS\n# ┌ ○ \u002F                          1.8 kB          96 kB   ← was 214 kB\n# trade-off: First Load JS counts bytes, not hydration cost. A route can shrink\n# in bytes while still hydrating hundreds of components — always confirm with a\n# trace, not with the table alone.\n","bash",[30,642,643,653,659,664,669,674],{"__ignoreMap":315},[319,644,645,649],{"class":205,"line":321},[319,646,648],{"class":647},"sQw3B","next",[319,650,652],{"class":651},"sZ8jY"," build\n",[319,654,655],{"class":205,"line":327},[319,656,658],{"class":657},"sjfSM","# Route (app)                    Size     First Load JS\n",[319,660,661],{"class":205,"line":333},[319,662,663],{"class":657},"# ┌ ○ \u002F                          1.8 kB          96 kB   ← was 214 kB\n",[319,665,666],{"class":205,"line":339},[319,667,668],{"class":657},"# trade-off: First Load JS counts bytes, not hydration cost. A route can shrink\n",[319,670,671],{"class":205,"line":345},[319,672,673],{"class":657},"# in bytes while still hydrating hundreds of components — always confirm with a\n",[319,675,676],{"class":205,"line":351},[319,677,678],{"class":657},"# trace, not with the table alone.\n",[15,680,681],{},"Then verify in the field. Segment INP by interactions that happened within two seconds of navigation: that population is the one paying the hydration cost, and it should improve first and most. Site-wide p75 follows more slowly, in proportion to how many sessions interact early.",[43,683,685],{"id":684},"faq","FAQ",[687,688,691,695],"details",{"className":689},[690],"faq-item",[692,693,694],"summary",{},"Does 'use client' mean the component is not server-rendered?",[15,696,697,698,701],{},"No. Client components are still rendered to HTML on the server; the directive controls whether they are also ",[92,699,700],{},"hydrated"," on the client and whether their code is shipped. That is why a page full of client components still paints fast and still fails INP — the markup arrives early, the JavaScript to make it interactive arrives and executes later.",[687,703,705,708],{"className":704},[690],[692,706,707],{},"Should I set ssr: false on dynamic imports to cut hydration?",[15,709,710,711,714],{},"Only for components whose content is genuinely not needed in the initial HTML — a chart that requires a browser-only library, for instance. ",[30,712,713],{},"ssr: false"," removes the server markup, so you lose the content for crawlers and for the first paint, and you usually introduce a layout shift when the component finally mounts. Prefer keeping SSR and deferring hydration instead.",[687,716,718,721],{"className":717},[690],[692,719,720],{},"How do I find which import pulled a module into the client bundle?",[15,722,723,724,726],{},"Trace the import chain from any client entry point in the build's module graph — the bundle analyser output shows which client chunk contains the module and what imports it. The most common surprise is a barrel file: importing one client component from an ",[30,725,297],{}," can pull in siblings that were meant to stay on the server. Import from the concrete module path instead.",[687,728,730,733],{"className":729},[690],[692,731,732],{},"How do I audit which components are client components?",[15,734,735],{},"Search the source for the directive and then follow the imports, because the boundary is transitive: every module a client module imports joins the client graph. A directive count alone understates the blast radius. The reliable check is the build's client bundle contents — if a component you believed was server-only appears there, something imported it across the boundary, and the import chain will show you what.",[687,737,739,742],{"className":738},[690],[692,740,741],{},"Are server components always faster?",[15,743,744,745,748],{},"For rendering, they remove client work entirely, which is unambiguously good. What they add is data-fetching latency on the server and a serialised payload travelling to the client, so a server component that fetches slowly can delay the whole response while a client component would have painted a shell first. The right split is by ",[92,746,747],{},"interactivity",", not by a belief that one kind is faster: static content on the server, controls on the client, and data fetched wherever it is fastest.",[687,750,752,755],{"className":751},[690],[692,753,754],{},"Does streaming remove the need to move the boundary?",[15,756,757,758,761],{},"It changes when the cost is paid, not how much there is. Streaming with ",[30,759,760],{},"Suspense"," lets parts of the page arrive and hydrate independently, so no single task blocks everything — a genuine improvement to interaction latency. But the same components still hydrate, so total blocking time barely moves. Use streaming to make a large tree feel responsive, and boundary placement to make the tree smaller.",[43,763,765],{"id":764},"related","Related",[48,767,768,773,780],{},[51,769,770,772],{},[19,771,22],{"href":21}," — the cost model and the other techniques for shrinking it.",[51,774,775,779],{},[19,776,778],{"href":777},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002Fimplementing-route-level-code-splitting-in-nextjs\u002F","Implementing route-level code splitting in Next.js"," — the chunk-boundary side of the same problem.",[51,781,782,786],{},[19,783,785],{"href":784},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002F","Deferring below-the-fold hydration with IntersectionObserver"," — a framework-agnostic version of step 4.",[788,789,791],"script",{"type":790},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Fix a long hydration task in the Next.js App Router\",\n  \"description\": \"Locate the high client boundary that converted a route into client components, push the directive to the leaves, shrink the flight payload, and defer non-critical widgets.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"position\": 1, \"name\": \"Push the directive down\", \"text\": \"Keep layouts and pages as server components and mark only interactive leaves with use client.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 2, \"name\": \"Pass children through providers\", \"text\": \"Render server children through the children prop so a client provider does not pull them across the boundary.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 3, \"name\": \"Shrink the flight payload\", \"text\": \"Send minimal props across the boundary and render large data as server HTML instead of serialising it twice.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 4, \"name\": \"Defer non-critical widgets\", \"text\": \"Load below-the-fold client components with next\u002Fdynamic while reserving their box to avoid layout shift.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router\u002F#step-by-step-resolution\" }\n  ]\n}\n",[788,793,794],{"type":790},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Fix a 500ms Hydration Task in the Next.js App Router\",\n  \"description\": \"Find the client boundary that turned a server-rendered route into a fully hydrated one, and push interactivity back to the leaves.\",\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\u002Ffixing-long-hydration-tasks-in-nextjs-app-router\u002F\" }\n}\n",[788,796,797],{"type":790},"\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\": \"Fixing Long Hydration Tasks in Next.js App Router\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router\u002F\" }\n  ]\n}\n",[799,800,801],"style",{},"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 .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 .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}",{"title":315,"searchDepth":327,"depth":327,"links":803},[804,805,806,807,808,809],{"id":45,"depth":327,"text":46},{"id":259,"depth":327,"text":260},{"id":301,"depth":327,"text":302},{"id":624,"depth":327,"text":625},{"id":684,"depth":327,"text":685},{"id":764,"depth":327,"text":765},"The App Router only hydrates client components — until one high-up 'use client' pulls the whole page back onto the main thread. Find it and push the boundary down.","md",{"slug":813,"type":814,"breadcrumb":815,"datePublished":823,"dateModified":823},"fixing-long-hydration-tasks-in-nextjs-app-router","article",[816,819,820,821],{"name":817,"url":818},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":822},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router\u002F","2026-08-01","\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router",{"title":826,"description":827},"Fixing Long Hydration Tasks in the Next.js App Router","Diagnose a 500ms+ hydration task in the App Router: find the client boundary that swallowed the tree, shrink the RSC payload, and push interactivity to the leaves.","javascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Ffixing-long-hydration-tasks-in-nextjs-app-router\u002Findex","SegqSeBMeWf2FGYKXuGetFAD30yQDeDpJoIRuRm10Y4",[831,835],{"title":832,"path":833,"stem":834,"children":-1},"Deferring Below-the-Fold Hydration with IntersectionObserver","\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver","javascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fdeferring-below-the-fold-hydration-with-intersectionobserver\u002Findex",{"title":836,"path":837,"stem":838,"children":-1},"Islands vs Full Hydration for Content Sites","\u002Fjavascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fislands-vs-full-hydration-for-content-sites","javascript-bundle-optimization-code-splitting\u002Fhydration-and-partial-rehydration-strategies\u002Fislands-vs-full-hydration-for-content-sites\u002Findex",1785622405980]