Tuning NuxtLink Prefetch Behavior
On listing and navigation-heavy pages, every <NuxtLink> that scrolls into view fires a route-chunk and payload prefetch, and forty visible links translate into eighty-plus background requests that compete with the LCP image and API calls still on the wire.
Root Cause: Visibility-Triggered Prefetch Scales with Link Count, Not User Intent
<NuxtLink> inherits its prefetching model from the idea that a link a user can see is a link a user might click. By default, Nuxt registers each rendered link with a shared IntersectionObserver; when the link enters the viewport (and the browser reports an idle period), Nuxt prefetches the lazy-loaded route chunk for the target page and, on payload-extracted sites, the route’s _payload.json data file as well. For a page with five links this is an excellent trade. For a category page, mega-menu, footer sitemap, or infinite-scroll feed, the trigger condition — visibility — is satisfied by dozens of links simultaneously at the exact moment the page is still loading its own critical resources.
The requests are issued at low network priority, which is the correct labeling but an incomplete defense. Dispatch priority orders requests leaving the queue; it does not partition bandwidth among responses already in flight. Once thirty chunk responses are streaming on the shared HTTP/2 or HTTP/3 connection, they consume congestion window and downlink bytes that the still-loading hero image and first-party API calls need — the same contention mechanics that govern prefetch semantics at the browser level apply regardless of how politely the framework schedules the trigger. On top of transfer cost, each arriving chunk is parsed and compiled on the main thread, and each payload is JSON-parsed, adding CPU work during the window that determines interactivity metrics.
The payload half of the prefetch is the part teams most often underestimate. A route chunk is shared across all pages of the same route pattern and caches once, but _payload.json is per destination URL: a product grid with 48 item links prefetches 48 distinct payload files, none of which are reusable for any other link. On data-rich pages, payload files routinely outweigh the route chunk itself, so the aggregate prefetch bill scales with catalog density rather than with anything the user will actually do — the median user clicks one of those 48 links, making roughly 98% of the prefetched bytes waste.
Visibility vs Interaction Trigger, on the Wire
Minimal Reproduction
A product grid is enough to reproduce the flood. Open the Network panel, filter to _nuxt/ and _payload.json, and load the page:
{{ product.name }} — {{ product.price }}
The fix keeps prefetching but moves the trigger from visibility to demonstrated intent, and sets the same policy globally so future pages inherit it:
{{ product.name }} — {{ product.price }}
// FIXED (site-wide): nuxt.config.ts — make interaction the default trigger
// so link-heavy pages stop paying a load-time tax for speculative bytes.
export default defineNuxtConfig({
experimental: {
defaults: {
nuxtLink: {
prefetch: true,
// visibility off: prefetch volume now scales with clicks, not links
prefetchOn: { visibility: false, interaction: true },
},
},
},
});
Deterministic Fix Protocol
- [ ] 1. Quantify the flood. Load the worst link-heavy page with the Network panel open, filter to
_nuxt/plus_payload.json, and record request count and total bytes fired in the first 5 seconds. This is your baseline waste figure. - [ ] 2. Confirm the overlap with critical resources. In a Performance panel trace, check whether prefetch transfers coincide with the LCP image’s download window. If the hero finishes before the burst begins, your problem is cache churn and CPU, not LCP — the fix is the same but the success metric differs.
- [ ] 3. Set the global default to interaction. Add the
experimental.defaults.nuxtLink.prefetchOnblock shown above tonuxt.config.ts. A global default beats per-link props because new templates inherit the policy without review vigilance. - [ ] 4. Re-enable visibility prefetch only where it earns its cost. Primary CTAs and the top navigation’s two or three highest-traffic targets can carry
:prefetch-on="{ visibility: true }"individually — a handful of high-click-probability links is what the visibility trigger was designed for. - [ ] 5. Verify the interaction trigger fires early enough. In the Network panel, hover a card link and confirm the chunk and payload requests dispatch on pointerenter, then click and confirm both are served from cache (memory or disk) at navigation time. If click-to-render still waits on the payload, your CDN’s TTFB on
_payload.jsonis the next target. - [ ] 6. Add connection-aware gating for the links that kept visibility prefetch. Nuxt already skips prefetching when the Network Information API reports
saveDataor a 2g-classeffectiveType; verify this by emulating Data Saver in DevTools (Network conditions drawer) and confirming zero prefetch requests. Extend the same check to any custom prefetch composables you ship. - [ ] 7. Re-measure the baseline page. Request count in the first 5 seconds should drop to roughly the route’s own resource count, LCP should recover the bandwidth the burst was consuming, and click-through navigation timing should remain within ~50 ms of the visibility-prefetch baseline.
Before/After Metrics
Lab conditions: throttled 4G (9 Mbps, 150 ms RTT), category page with 48 product links, payload extraction enabled, warm CDN. “Before” is default visibility prefetching; “after” is interaction-triggered prefetch site-wide.
| Metric | visibility (default) | interaction | Change |
|---|---|---|---|
| Requests in first 5 s | 131 | 39 | −70% |
| Speculative bytes transferred | 2.9 MB | 0 KB at load | −2.9 MB |
| LCP | 3,010 ms | 1,720 ms | −43% |
| Main-thread script parse (load window) | 480 ms | 165 ms | −66% |
| Click → route render (hovered link) | 90 ms | 130 ms | +40 ms |
| Click → route render (cold link, no hover) | 90 ms | 340 ms | +250 ms |
The last row is the honest cost: a keyboard user or instant-clicker who gives no hover signal pays a cold fetch at click time. On a warm HTTP/3 connection that penalty is a few hundred milliseconds on one navigation — against a multi-second LCP improvement on every landing. Sites that want to close even that gap can pair interaction-triggered chunks with speculative prefetching rules for full documents on their highest-traffic next pages.
FAQ
Does switching to interaction-based prefetch make navigation feel slower?
Rarely, and usually unmeasurably. Desktop users hover 150–400 ms before clicking, which covers a route chunk and payload fetch from a warm CDN connection; on touch devices the trigger is touchstart, which still precedes the navigation commit by tens of milliseconds. The users who genuinely pay are those who click with zero prior signal — and their one-time penalty is far smaller than the load-time cost that visibility prefetching imposed on everyone.
Prefetch requests are Low priority — why do they still hurt loading?
Because priority governs when requests leave the queue, not how bandwidth is shared once responses stream. Thirty in-flight prefetch responses on the same HTTP/2 or HTTP/3 connection consume congestion window, downlink bytes, and — after arrival — main-thread parse time, all during the window in which the LCP image and first-party data calls are also transferring. Low priority makes the flood polite at dispatch; it does not make it free.
How do I disable NuxtLink prefetching for the whole site?
Use the defaults block in nuxt.config.ts: experimental.defaults.nuxtLink.prefetch: false turns the machinery off entirely, while prefetchOn: { visibility: false, interaction: true } keeps prefetching but gates it behind hover, focus, or touch. The second option is almost always the better default — you retain near-instant navigations for users who signal intent while eliminating the per-link load-time tax.
Related
- Nuxt Resource Loading Optimization — the parent section covering Nuxt’s full loading pipeline: payloads, hydration, and asset hints
- Mastering Link Rel Preload & Prefetch — the browser-level prefetch semantics NuxtLink builds on
- Framework-Specific Loading Strategies — up to the topic-area root