Astro Info
Astro v6.2.1
Vite v7.3.2
Node v22.22.2
System Linux (x64)
Package Manager npm
Output static
Adapter none
Integrations none
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
When an anchor has any inner child element — <span>, <img>, <svg>, an Astro <Image />, or any card-style wrapper — data-astro-prefetch="tap" silently fires no prefetch. The user taps or clicks the visible content, nothing is prefetched, and the page loads cold.
Root cause (packages/astro/src/prefetch/index.ts)
initTapStrategy() registers touchstart and mousedown on document via event delegation:
// lines 57–69
function initTapStrategy() {
for (const event of ['touchstart', 'mousedown']) {
document.addEventListener(event, (e) => {
if (elMatchesStrategy(e.target, 'tap')) {
prefetch(e.target.href, { ignoreSlowConnection: true });
}
}, { passive: true });
}
}
For touchstart/mousedown, e.target is always the deepest element under the pointer. elMatchesStrategy (line 267) rejects anything that is not directly an <A> tag:
function elMatchesStrategy(el, strategy): el is HTMLAnchorElement {
if (el?.tagName !== 'A') return false; // ← inner child fails here
...
}
So tapping on <a><span>Home</span></a> delivers e.target = <span>, elMatchesStrategy returns false, and nothing prefetches.
Why only tap is affected
Every other strategy avoids the problem by not using document-level delegation for pointer events:
hover attaches mouseenter/mouseleave directly to each <a> — mouseenter does not bubble.
viewport and load iterate document.getElementsByTagName('a') directly.
Only tap delegates to document, where e.target is the descendant.
The slow-connection fallback doubles the impact
elMatchesStrategy (line 278) falls back every strategy to tap when
navigator.connection.saveData is true or effectiveType matches 2g:
if (strategy === 'tap' && (attrValue != null || prefetchAll) && isSlowConnection()) {
return true;
}
This means prefetchAll: true users on 2G or data-saver mode get zero prefetch on any link with inner markup — the exact population that most needs prefetch to work.
Affected patterns (all extremely common)
<!-- styled text -->
<a href="/about" data-astro-prefetch="tap"><span>About</span></a>
<!-- image link -->
<a href="/gallery" data-astro-prefetch="tap"><img src="cover.jpg" /></a>
<!-- Astro Image component -->
<a href="/post" data-astro-prefetch="tap"><Image src={cover} alt="..." /></a>
<!-- SVG icon link -->
<a href="/home" data-astro-prefetch="tap"><svg>...</svg> Home</a>
<!-- card link -->
<a href="/article" data-astro-prefetch="tap">
<div><h2>Title</h2><p>Description</p></div>
</a>
What's the expected result?
data-astro-prefetch="tap" should fire a prefetch whenever the user taps or clicks anywhere inside a prefetch-enabled anchor, regardless of whether e.target is the <a> itself or a descendant element. The fix is a one-line
change in initTapStrategy: walk up to the nearest ancestor <a> using closest() before passing to elMatchesStrategy:
const anchor = (e.target as Element | null)?.closest?.('a');
if (anchor && elMatchesStrategy(anchor, 'tap')) {
prefetch(anchor.href, { ignoreSlowConnection: true });
}
Link to Minimal Reproducible Example
https://github.com/Hunnyboy1217/prefetch-tap-nested-repro
Participation
Astro Info
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
When an anchor has any inner child element —
<span>,<img>,<svg>, an Astro<Image />, or any card-style wrapper —data-astro-prefetch="tap"silently fires no prefetch. The user taps or clicks the visible content, nothing is prefetched, and the page loads cold.Root cause (
packages/astro/src/prefetch/index.ts)initTapStrategy()registerstouchstartandmousedownondocumentvia event delegation:For
touchstart/mousedown,e.targetis always the deepest element under the pointer.elMatchesStrategy(line 267) rejects anything that is not directly an<A>tag:So tapping on
<a><span>Home</span></a>deliverse.target = <span>,elMatchesStrategyreturnsfalse, and nothing prefetches.Why only
tapis affectedEvery other strategy avoids the problem by not using document-level delegation for pointer events:
hoverattachesmouseenter/mouseleavedirectly to each<a>—mouseenterdoes not bubble.viewportandloaditeratedocument.getElementsByTagName('a')directly.Only
tapdelegates todocument, wheree.targetis the descendant.The slow-connection fallback doubles the impact
elMatchesStrategy(line 278) falls back every strategy totapwhennavigator.connection.saveDatais true oreffectiveTypematches2g:This means
prefetchAll: trueusers on 2G or data-saver mode get zero prefetch on any link with inner markup — the exact population that most needs prefetch to work.Affected patterns (all extremely common)
What's the expected result?
data-astro-prefetch="tap"should fire a prefetch whenever the user taps or clicks anywhere inside a prefetch-enabled anchor, regardless of whethere.targetis the<a>itself or a descendant element. The fix is a one-linechange in
initTapStrategy: walk up to the nearest ancestor<a>usingclosest()before passing toelMatchesStrategy:Link to Minimal Reproducible Example
https://github.com/Hunnyboy1217/prefetch-tap-nested-repro
Participation