Skip to content

Prefetch tap strategy silently skips links with nested children (<span>, <img>, <svg>) #16564

@hunnyboy1217

Description

@hunnyboy1217

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

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P4: importantViolate documented behavior or significantly impacts performance (priority)pkg: astroRelated to the core `astro` package (scope)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions