vercel / vercel/next.js

Link Component does not prefetch on network recovery.

Open
#69,999 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug create-next-app Performance
Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

Link to the code that reproduces this issue

https://github.com/refirst11/reproduction-app

To Reproduce

Sometimes unnecessary mounts are run.
And canary release e2e testing is special.

Let's say the start of the route is "/" and it goes offline in this state.
"/a", "/b", and "/c" are in the viewport at this point, so there will be a smooth transition when transitioning to them.
And it is not possible to transition to links "/d", "/e", and "/f" on pages a, b, and c at this point, but let's say the network is restored, for example in a subway or train tunnel. In that case, when transitioning to pages d, e, and f, a network request will be sent and the page will be reloaded as in the a tag.
I call this mounting on recovery in Next.js.

However, since prefetch only needs to hit when the network restarts,
I thought it would be fixed by adding the following to the dependencies in React.useEffect.

In my environment, when I tried e2e testing next.js,
pnpm test-dev test/e2e/app-dir/app-prefetch/prefetching.test.ts

It's possible that I overlooked this, but all of the test code in the describe block of prefetching.test.ts passes (even if it fails), so I decided to write this as an issue here.

Current vs. Expected behavior

When the network becomes online, link components in the viewport are prefetched.

Provide environment information
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.0.0: Sat Jul 13 00:56:26 PDT 2024; root:xnu-11215.0.165.0.4~50/RELEASE_ARM64_T8103
  Available memory (MB): 16384
  Available CPU cores: 8
Binaries:
  Node: 22.8.0
  npm: 10.8.2
  Yarn: N/A
  pnpm: 9.6.0
Relevant Packages:
  next: 14.2.5 // There is a newer version (14.2.9) available, upgrade recommended! 
  eslint-config-next: 13.5.6
  react: 18.3.1
  react-dom: 18.3.1
  typescript: 5.5.4
Next.js Config:
  output: N/A
Which area(s) are affected? (Select all that apply)

create-next-app, Performance

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

next dev (local), next start (local), Vercel (Deployed), Other (Deployed)

Additional context

Deploy is with vercel and next start.
I've been using Next.js since version 11, but even back then there was no prefetching during network restarts.

I want to improve

// use-network.ts
import { useSyncExternalStore } from 'react'

function subscribe(callback: () => void) {
  window.addEventListener('online', callback)
  window.addEventListener('offline', callback)
  return () => {
    window.removeEventListener('online', callback)
    window.removeEventListener('offline', callback)
  }
}

export function useNetwork() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true
  )
}
// link.tsx
...
    const [setIntersectionRef, isVisible, resetVisible] = useIntersection({
      rootMargin: '200px',
    })

    const isOnline = useNetwork()
    
    React.useEffect(() => {
      // in dev, we only prefetch on hover to avoid wasting resources as the prefetch will trigger compiling the page.
      if (process.env.NODE_ENV !== 'production') {
        return
      }

      if (!router) {
        return
      }

      // If we don't need to prefetch the URL, don't do prefetch.
      if (!isVisible || !isOnline || !prefetchEnabled) {
        return
      }

      // Prefetch the URL.
      prefetch(
        router,
        href,
        as,
        { locale },
        {
          kind: appPrefetchKind,
        },
        isAppRouter
      )
    }, [
      as,
      href,
      isVisible,
      isOnline,
      locale,
      prefetchEnabled,
      pagesRouter?.locale,
      router,
      isAppRouter,
      appPrefetchKind,
    ])

e2e test case

  describe('online/offline transitions', () => {
    if (isNextDeploy) return

    it('should prefetch links in viewport after network recovery', async () => {
      const requests: string[] = []
      let isOffline = false

      const browser = await next.browser('/', {
        async beforePageLoad(page) {
          // Intercept RSC prefetch requests and abort them while offline
          await page.route('**/*', (route) => {
            const url = new URL(route.request().url())
            if (isOffline && url.searchParams.has('_rsc')) {
              route.abort('internetdisconnected')
            } else {
              route.continue()
            }
          })

          // Track all RSC requests including aborted ones
          page.on('request', async (req) => {
            const url = new URL(req.url())
            if (url.searchParams.has('_rsc')) {
              requests.push(url.pathname)
            }
          })
        },
      })

      // Simulate going offline: spoof navigator.onLine and dispatch the offline event
      isOffline = true
      await browser.eval(`
      Object.defineProperty(navigator, 'onLine', { get: () => false, configurable: true })
      window.dispatchEvent(new Event('offline'))
    `)
      await waitFor(300)

      // Expand the accordion to make the /dashboard link visible while offline
      await browser.elementByCss('#accordion-to-dashboard').click()
      await browser.waitForElementByCss('#to-dashboard')

      const requestsCountBeforeRecovery = requests.filter((req) =>
        req.includes('/dashboard')
      ).length

      // Simulate network recovery: restore navigator.onLine and dispatch the online event.
      // This should cause useNetwork() to return true, triggering the prefetch useEffect to re-run.
      isOffline = false
      await browser.eval(`
      Object.defineProperty(navigator, 'onLine', { get: () => true, configurable: true })
      window.dispatchEvent(new Event('online'))
    `)

      await browser.waitForIdleNetwork()

      // Verify that prefetch was retried after the online event
      await retry(async () => {
        const dashboardRequests = requests.filter((req) =>
          req.includes('/dashboard')
        )
        expect(dashboardRequests.length).toBeGreaterThan(
          requestsCountBeforeRecovery
        )
      })
    })
  })

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running pnpm test-dev test/e2e/app-dir/app-prefetch/prefetching.test.ts and read its online/offline transitions case. Then trace the Link prefetch logic in link.tsx and the reproduction app linked in the issue. Done means the viewport link issues a new prefetch request after the online event, with the e2e test passing.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nextjs, react, typescript
Domain
frontend, performance, testing-qa
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.