next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Prefetch requests happen more than once on Next 16

Open jperezr21 opened this issue 1 month ago • 12 comments

Link to the code that reproduces this issue

https://github.com/jperezr21/next-16-issue

To Reproduce

  1. pnpm build
  2. pnpm start
  3. Go to /
  4. See logs
Image
  1. git checkout next-15
  2. pnpm build --turbopack
  3. pnpm start
  4. Go to /
  5. See logs
Image

Current vs. Expected behavior

Links should be prefetched once, but they're being prefetched more than once

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.6.0: Mon Aug 11 21:16:31 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6030
  Available memory (MB): 18432
  Available CPU cores: 11
Binaries:
  Node: 22.13.1
  npm: 11.1.0
  Yarn: N/A
  pnpm: 10.20.0
Relevant Packages:
  next: 16.0.1 // Latest available version is detected (16.0.1).
  eslint-config-next: N/A
  react: 19.2.0
  react-dom: 19.2.0
  typescript: 5.9.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Linking and Navigating

Which stage(s) are affected? (Select all that apply)

Vercel (Deployed), next start (local)

Additional context

Identified this issue after upgrading to next 16 and seeing the number of function invocations in Vercel more than double:

Image

Upgraded on Oct 23, downgraded on Oct 27

jperezr21 avatar Oct 29 '25 01:10 jperezr21

Related: https://github.com/vercel/next.js/issues/85470

jperezr21 avatar Oct 29 '25 02:10 jperezr21

~~I opened a PR for another issue (seems to be similar). The fix ensures that the prefetch returns a 304 status code (this was the original issue) and it seems that with this fix the prefetch only happens once. See: https://github.com/vercel/next.js/pull/85643~~ nevermind, it had a different root cause. opened a PR

denesbeck avatar Nov 01 '25 10:11 denesbeck

This is actually working as intended. If you inspect the network requests going to the Next.js server, they send different values for the request headers. These request different files/parts/segments from Next.js, and do not represent duplicate requests.

This is due to Next.js 16 making the Client Segment Cache the new mechanism that we use to request/store segment data for prefetching. These prefetch requests made to routes that are static or partially static will not trigger an invocation, and instead will be served out of the cache.

wyattjoh avatar Nov 14 '25 21:11 wyattjoh

Well, it's inefficient and expensive. Why are 4 requests needed to prefetch a page with a single div? https://github.com/jperezr21/next-16-issue/commit/1d2fc6ea336a52be9934a36db40d5fcb0c1c8fbd#diff-ef0db79d016a2379338ad88cf190a5b9a543b378ad06dbc1a78ce69c5ce77c15

jperezr21 avatar Nov 15 '25 01:11 jperezr21

I can confirm this issue and would like to add an important detail about the severity: prefetch requests are blocking user navigation.

Additional Impact: Navigation Blocking

In our production application, we're experiencing the same duplicate prefetch behavior, but with a critical UX issue:

  • Navigation is blocked when users click a link before the prefetch request completes
  • Multiple prefetch requests for /c/brands?_rsc= can take 5+ seconds to complete
  • If a user clicks the brands link during this time, navigation is delayed until all prefetch requests finish
  • This creates a very poor user experience where the site feels unresponsive
Image

Our Setup

  • Next.js: 16.0.1
  • React: 19.2.0
  • Navigation: Using next-intl's Link component wrapped in custom components
  • Affected Route: /c/brands (complex server component with multiple Suspense boundaries)

Reproduction

  1. Build: pnpm build
  2. Start: pnpm start
  3. Navigate to home page
  4. Open DevTools Network tab, filter for _rsc=
  5. Observe multiple prefetch requests for the same route
  6. Click the link before prefetch completes ← Navigation is blocked here
  7. Notice the delay until prefetch finishes

Network Behavior

We see multiple requests like:

GET /c/brands?_rsc=abc123
GET /c/brands?_rsc=def456
GET /c/brands?_rsc=ghi789

arfa123 avatar Nov 16 '25 08:11 arfa123

The reason that you're seeing different values for the ?_rsc= query parameter is that request headers like Next-Router-State-Tree, Next-URL, RSC, Next-Router-Prefetch and Next-Router-Segment-Prefetch are different! These requests are loading small files, and the client deduplicates requests for the same layout shared between different links. For example, if you had 10 links to different "products" that all shared the same layout, we'd only fetch the shared layouts once, reducing the total bandwidth transferred over the network.

We'll definitely take a look at any strange navigation behaviours though!

wyattjoh avatar Nov 17 '25 18:11 wyattjoh

Please take a look at @carlos-dubon's issue https://github.com/vercel/next.js/issues/85470. Upgrading caused the number of requests to triple and latency to suffer. I get that with proper use of cache components this can probably be mitigated. But you should communicate clearly that upgrading without implementing these changes has these negative effects. This is a breaking change and it was not communicated clearly.

jperezr21 avatar Nov 17 '25 21:11 jperezr21

Please take a look at @carlos-dubon's issue #85470. Upgrading caused the number of requests to triple and latency to suffer. I get that with proper use of cache components this can probably be mitigated. But you should communicate clearly that upgrading without implementing these changes has these negative effects. This is a breaking change and it was not communicated clearly.

It does not help with Cache Components, I am going to write a new bug report about it.

thernstig avatar Nov 25 '25 16:11 thernstig

Another downside of all these new prefetch requests is that they compete with the LCP element making this Core Web Vitals metric worse than it was before v16

Our team globally disabled prefetch and we got a boost in our PageSpeed Insights score in v15 (we went from ~85 points to ~98)

carlos-dubon avatar Dec 01 '25 22:12 carlos-dubon

Another downside of all these new prefetch requests is that they compete with the LCP element making this Core Web Vitals metric worse than it was before v16

Our team globally disabled prefetch and we got a boost in our PageSpeed Insights score in v15 (we went from ~85 points to ~98)

I confirm that this extra prefetching is affecting the Core Web Vitals score.

arfa123 avatar Dec 02 '25 05:12 arfa123

Our team globally disabled prefetch

@carlos-dubon, how did you do this?

kachkaev avatar Dec 02 '25 13:12 kachkaev

@kachkaev

Our team globally disabled prefetch

@carlos-dubon, how did you do this?

I wrapped Next.js' Link component a long time ago. So it was pretty easy to disable it globally.

This is how my current Link.tsx file looks like, feel free to modify it as you need:

'use client';
// eslint-disable-next-line no-restricted-imports
import NextLink from 'next/link';
import NProgress from 'nprogress';
import { useEffect, useTransition } from 'react';
import { useRouter } from 'next/navigation';

const isModifiedClick = (e: React.MouseEvent) => {
    return e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0;
};

type LinkProps = {
    hideProgressBar?: boolean;
    preventNavigation?: boolean;
    stopPropagation?: boolean;
} & Parameters<typeof NextLink>[0];

export default function Link({ hideProgressBar, preventNavigation, stopPropagation, ...props }: LinkProps) {
    const router = useRouter();

    const [isPending, startTransition] = useTransition();

    useEffect(() => {
        if (hideProgressBar) return;
        if (isPending) {
            NProgress.start();
        } else {
            NProgress.done();
        }
    }, [isPending, hideProgressBar]);

    return (
        <NextLink
            {...props}
            prefetch={false}
            onClick={e => {
                props.onClick?.(e);

                if (props.target === '_blank' || isModifiedClick(e)) {
                    return;
                }

                e.preventDefault();

                if (preventNavigation) return;
                if (stopPropagation) e.stopPropagation();

                startTransition(() => {
                    const url = props.href.toString();
                    const options = {
                        scroll: props.scroll ?? true,
                    };

                    if (props.replace) {
                        router.replace(url, options);
                    } else {
                        router.push(url, options);
                    }
                });
            }}
        >
            {props.children}
        </NextLink>
    );
}

carlos-dubon avatar Dec 02 '25 14:12 carlos-dubon