Drop a loading.js beside a page.js and Next.js 10,514 wraps that page — and everything routed below it — in a <Suspense> boundary whose fallback is your component. It takes no props. The demo's /reports page awaits 1.2 seconds of data before it can render anything; reading the response as a stream shows the server answering immediately anyway:
--- chunk 0 (+1ms, 5133 bytes) <nav>...</nav><p id="clock">layout alive for 0s</p> <main><!--$?--><template id="B:0"></template> <div id="reports-skeleton"><h1>Quarterly reports</h1> <p>Loading the last two quarters...</p></div><!--/$--></main> --- chunk 1 (+1192ms, 330 bytes) --- chunk 2 (+1194ms, 1229 bytes)
That first chunk is the entire visible page one millisecond in: nav, clock and skeleton, painted while the query still runs. The same fallback appears on a client-side navigation, and that is where prefetching pays off — because /reports has a loading.js, its skeleton reached the browser before the click. Sampling the page 120 ms after the click, then once it settled, gives:
main at +120ms : Quarterly reports | | Loading the last two quarters... main when settled : Quarterly reports | Q1 revenue 412,900 | Q2 revenue 508,300
Three consequences follow from loading.js being a Suspense boundary in a fixed position. It wraps page.js, not-found.js and nested layouts, but not the layout.js, template.js or error.js of its own segment — so a layout that awaits uncached data blocks the navigation and no fallback appears; move that fetch into the page or into its own <Suspense>. Navigation stays interruptible: clicking a third link while the skeleton is up abandons the pending render. And the shared layout stays interactive, which is why the clock keeps counting under the skeleton.
Design the fallback to match the shape of what replaces it: a skeleton 400 pixels shorter than the real content shoves everything below it down at the swap, a Cumulative Layout Shift penalty measured as Performance describes. Reserve the height and show structure, not a spinner.