Mastering Next.js Prefetching: Enhance Navigation with the SuperLink Component
Build a custom component that does link prefetch on hover
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 viewportfalse
– 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. 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 pagesfalse
– prefetching is completely disablednull
– 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
:
"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:
"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 Twitter𝕏 argue that this is a poor choice because moving your cursors across the page will trigger multiple prefetches. It's true. However, with justprefetch={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!
Will adding handlers for onPointerEnter
, onTouchStart
and onFocus
do the job? I'm no accessibility expert, but it certainly seems to be working:
"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.
navigator.connection
requires installing and configuring TypeScript types from network-information-types
package.I've omitted the rest of the code for brevity, but here's the gist of the idea:
"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!