# Mastering Next.js Prefetching: Enhance Navigation with the SuperLink Component

Next.js has a component that enables prefetching and client-side navigation between routes: `Link`. It's one of the primitives upon which you build applications. Yet, there are a lot of misconceptions about how prefetching actually works. To add to the confusion, its behavior has been changing since Next.js 13 beta. **How does the prefetch actually work? And can we make it more flexible?**

## The Link Component in the Pages Router

The `Link` component from `next/link` accepts an optional prop `prefetch`. Back in Next.js 12 or now **in Pages Router**, `prefetch` took only two values: `true` or `false` (and it was `true` by default when omitted). However, these values don't really mean what you might think they do.

* `true` – the route is prefetched when the link enters the viewport
    
* `false` – the route is **not** prefetched when the link enters the viewport; however, the **route will be prefetched on hover**
    

That last behavior was commonly perceived as confusing because the semantics of the `false` value didn't obviously convey the mechanics. Also, it was impossible to disable the prefetching completely.

## The Link Component in the App Router

This changed with the introduction of App Router in Next.js 13. However, the exact mechanisms of the automatic prefetching were modified between Next 13 and Next 15, and finally, [a third possibility was introduced](https://github.com/vercel/next.js/commit/5d21c6cba13606892bc4740b82e2b7ac682905dc). The workings of Link are now also different, depending on **whether it points to a static or dynamic page.** The `prefetch` prop is (still) optional and takes the following values:

* `true` – the full route is prefetched when the link enters the viewport, both for static and dynamic pages
    
* `false` – prefetching is completely disabled
    
* `null` – **new default value** – the behavior depends on whether the route is static or dynamic:
    
    * static – the full route will be prefetched when the link enters the viewport
        
    * dynamic – only a partial route down to the nearest segment with a `loading.tsx` boundary will be prefetched; **if there's no such route, nothing is prefetched**
        

Now, prefetching is more powerful and adjustable. However, it still lacks some flexibility. Let's fix that!

## SuperLink: Wrapper for Link

We can extend the capabilities of the `Link` and control prefetching by introducing a custom component that wraps around the `Link`.

The plan is to wrap around it, override `prefetch`, and programmatically prefetch on certain interactions. My idea is to do it when a user hovers over the element. **Often, the time between hover and click is long enough for the prefetch to complete, and then navigating to the route is instantaneous.** When we're done with that, we'll also consider other ways of interacting with the web app, and improve accessibility of our solution. Let's get to it!

## Implementing SuperLink with onMouseEnter

Let's start by wrapping around `next/link`. We're using **React 19**, so we don't need the whole `forwardRef` shenanigans, and we can simply accept `ref` as props via `ComponentPropsWithRef`:

```typescript
"use client";

import Link from "next/link";
import { type ComponentPropsWithRef } from "react";

export const SuperLink = (props: ComponentPropsWithRef<typeof Link>) => {
  return <Link {...props} />;
};
```

Now, let's override the `prefetch` prop and use `router` to programmatically prefetch instead. We'll also need to handle the fact that `href` can be a string or a URLObject:

```typescript
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import { type ComponentPropsWithRef } from "react";

export const SuperLink = (props: ComponentPropsWithRef<typeof Link>) => {
  const router = useRouter();
  const strHref = typeof props.href === "string" ? props.href : props.href.href;
  return (
    <Link
      {...props}
      prefetch={false}
      onMouseEnter={(e) => {
        if (strHref) {
          void router.prefetch(strHref);
        }
        return props.onMouseEnter?.(e);
      }}
    />
  );
};
```

Now, prefetching happens on `mouseenter`, which should be enough to make the navigation smoother than no prefetching at all. It also works for both static and dynamic routes.

## Unnecessary requests

Some people on <s>Twitter</s>**𝕏** argue that this is a poor choice because moving your cursors across the page will trigger multiple prefetches. It's true. **However, with just**`prefetch={true}`**, even more requests will be made**. So, I'd say our current implementation is an improvement anyway. On rare occasions, you might want to change the event or condition for which the `prefetch` is called. It's a tradeoff.

## Accessibility

Our current solution completely ignores users navigating the application with keyboards and touch screens. We should improve it and work on accessibility, too!

<div data-node-type="callout">
<div data-node-type="callout-emoji">👏</div>
<div data-node-type="callout-text">Shout out to <a target="_blank" rel="noopener noreferrer nofollow" href="https://x.com/ImLunaHey" style="pointer-events: none">ImLunaHey</a>, <a target="_blank" rel="noopener noreferrer nofollow" href="https://x.com/RedCardinal" style="pointer-events: none">RedCardinal</a>, and <a target="_blank" rel="noopener noreferrer nofollow" href="https://x.com/JohnPhamous" style="pointer-events: none">JohnPhamous</a> for reminding me of how important accessibility is.</div>
</div>

Will adding handlers for `onPointerEnter`, `onTouchStart` and `onFocus` do the job? I'm no accessibility expert, but it certainly seems to be working:

```typescript
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import { type ComponentPropsWithRef } from "react";

export const SuperLink = (props: ComponentPropsWithRef<typeof Link>) => {
  const router = useRouter();
  const strHref = typeof props.href === "string" ? props.href : props.href.href;

  const conditionalPrefetch = () => {
    if (strHref) {
      void router.prefetch(strHref);
    }
  };

  return (
    <Link
      {...props}
      prefetch={false}
      onMouseEnter={(e) => {
        conditionalPrefetch();
        return props.onMouseEnter?.(e);
      }}
      onPointerEnter={(e) => {
        conditionalPrefetch();
        return props.onPointerEnter?.(e);
      }}
      onTouchStart={(e) => {
        conditionalPrefetch();
        return props.onTouchStart?.(e);
      }}
      onFocus={(e) => {
        conditionalPrefetch();
        return props.onFocus?.(e);
      }}
    />
  );
};
```

Now, again, you might argue that calling prefetch on `focus` will trigger many unnecessary requests. To get to the third link on the page with your keyboard, you need to also focus the first and second ones, and, in effect, you'll prefetch them, too. **I agree this is not optimal and can be improved. Looking forward to your suggestions on how.**

## Prefetch on Fast Internet Only

Here's another idea: only trigger `prefetch` when the user has a stable and fast internet connection. How can we do that? Using `navigator.connection`. Let's try that.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Using <code>navigator.connection</code> requires installing and configuring TypeScript types from <code>network-information-types</code> package.</div>
</div>

I've omitted the rest of the code for brevity, but here's the gist of the idea:

```typescript
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import { type ComponentPropsWithRef } from "react";

export const SuperLink = (props: ComponentPropsWithRef<typeof Link>) => {
  const router = useRouter();
  const strHref = typeof props.href === "string" ? props.href : props.href.href;
  return (
    <Link
      {...props}
      prefetch={false}
      onMouseEnter={(e) => {
        const hasFastInternet =
          !navigator.connection || navigator.connection.effectiveType === "4g";
        if (strHref && hasFastInternet) {
          void router.prefetch(strHref);
        }
        return props.onMouseEnter?.(e);
      }}
    />
  );
};
```

## Summary

It's not ideal, but remember the Pareto rule: We've **done 80% of the job in 20% of the time.** I'm open to suggestions on how this can be improved. Maybe with a debounce of some sort? Post your code in the comments!
