Repository navigation
Partial Prefetching: skip the embedded runtime prefetch (p) for blocking-metadata bot renders, and when it repeats the static shell
#100177
trevorlasn
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Goals
pfield) out of documents rendered with blocking metadata for a user agent that matcheshtmlLimitedBots. Those agents never hydrate, so the second render and its bytes are pure cost.Non-Goals
<Link prefetch={true}>resolves.htmlLimitedBotslist.Background
With
cacheComponents: trueandpartialPrefetching: trueon Next.js 16.4.0, a production document render takes thecacheComponents && cachedNavigationsbranch inserver/app-render/app-render.js. In Partial mode the branch waits forcacheSignal.cacheReady(), runsspawnRuntimePrefetchWithFilledCaches, and embeds the result as the payload'spfield, so the client router can cache runtime-prefetchable content during hydration.getPrefetchingModeForPagereturns Partial as soon asrenderOpts.partialPrefetchingis set, before it reads any segmentprefetchexport.base-server.jssetsrenderOpts.partialPrefetchingonce, from the config. Nothing on this path reads the user agent;serveStreamingMetadataonly decides where the metadata goes. A segment'sprefetchexport governs links to that segment. So an app cannot keep a document free of the embed without dropping Partial Prefetching for every link to the route.16.5.0-canary.10has the same gate.Two cases where nobody reads the stream:
htmlLimitedBotsis rendered with blocking metadata for an agent that does not run JavaScript. It never hydrates, so the second render and the embedded bytes are wasted. A serverless function returns the document uncompressed, so the bytes are paid in full between the function and the edge.connection()below a Suspense boundary. The stream then repeats the root layout and the static shell that the document already carries. On our routes its largest rows are<html>,<body>, the footer, a search trigger, a toaster and the page skeletons. Its only page-specific content is the route parameter and one title.Measured on 16.4.0 with
next buildandnext start: one build, withpartialPrefetchingon, and off atnext start. The documents were requested with the bot user agentmeta-externalagent/1.1, and the bytes are the document as served. Row 0 carries"p":"$<id>", and the stream is the<id>:o<hex>,chunks, so the "Stream" column is the sum of those chunk lengths. CPU is the server process's CPU time per document, over 20 alternating request pairs.partialPrefetching: true, raw / gzip bytesfalse, raw / gzip bytes/event/[slug]/profile/[username]/sports/skill-leaderboardBrowsers get the same stream: 15.5-25.5% of each of these documents gzipped. In production we serve about 28,000 crawler documents a day, each 0.5-1.0 MB uncompressed.
The alternatives available today:
partialPrefetching: false. This applies app-wide, or per route by exportingprefetch = 'partial'from every other route. It removes the stream but regresses navigation for readers. On a page with 126 links to one dynamic route, prefetching went from 5 requests and 7,843 B to 8 requests and 25,432 B. A tap on an unprefetched link took 85-96 ms instead of 44-50 ms to commit the URL. The docs also say the option goes away in the next major.experimental.cachedNavigations: false. It skips the branch, but it also drops the hydration cache of the static stage and the staged navigation responses on every route.connection()early for bot requests. It only moves where the runtime prerender stops. The layout and shell that the stream repeats sit above that point.Proposal
serveStreamingMetadatais false because the user agent matchedhtmlLimitedBots, do not createstartRuntimePrefetchRender, and leavepout of that response. The render already knows it is serving an agent that will not hydrate.l.A fix can be checked with the same reads: request a document with a bot user agent, and sum the chunk lengths of the row that
pnames.All reactions