Single page applications allow developers to create a set of pages that use soft navigations, or navigations that don't require a full page reload. In a soft navigation, the application replaces just the main content with the new content with the same document, optionally loading only incremental JavaScript + CSS needed for that page. This creates a smoother experience for users since it is faster and the page doesn't unload and reload.
However, not all links can use soft navigation. Links to any page served by a different application, such as links to other domains, will use hard navigations. Hard navigations unload the current document and all resources on the current page and then loads the new document and all subresources from that page. A user is likely to notice a hard navigation from additional latency and a flash of content on the screen.
The impact of hard navigations can be reduced by using prefetching and prerendering.
- Prefetching - Fetch the new document and potentially the subresources used by that document. (MDN)
- Prerendering - In addition to prefetching the content, the browser will render the new page in an invisible background tab as if a user visited it directly so that it is ready to be swapped out with the current page. (MDN)
Prefetching reduces latency by caching some content from the new page so the user doesn't have to wait for it when they click on the link. However, the user will still have to wait for some subresources to be downloaded and for the browser to render the page. Prerendering consumes more network and machine resources, but allows users to experience an instant navigation to that new page because it is already fully rendered in the background.
Optimizing hard navigations can help users have a better experience on your site by improving the performance of links that use hard navigations.
The Speculation Rules API is a new browser API that allows you to specify rules for which links on the page should be prefetched or prerendered. By inserting a script tag into your page or using a header, you can easily prefetch or prerender any link that would cause a hard navigation.
The script tag can use a set of rules for when the browser should prefetch or prerender a link.
<script type="speculationrules"> { "prerender": [ { "where": { "and": [ { "href_matches": "/*" }, { "not": { "href_matches": "/logout" } }, { "not": { "href_matches": "/*\\?*(^|&)add-to-cart=*" } }, { "not": { "selector_matches": ".no-prerender" } }, { "not": { "selector_matches": "[rel~=nofollow]" } } ] } } ], "prefetch": [ { "urls": ["next.html", "next2.html"], "requires": ["anonymous-client-ip-when-cross-origin"], "referrer_policy": "no-referrer" } ] }</script>
These rules can use a hardcoded list of paths or can target specific links by using selectors. Additionally, you can easily change the behavior for when prerendering should be used to conserve resources, such as when a user only hovers over a link or starts to click on a link.
If using Next.js or React, a simple component can be used to insert Speculation Rules into the page:
import Script from 'next/script';
export function SpeculationRules({ prefetchPathsOnHover, prerenderPathsOnHover,}) { const speculationRules = { prefetch: [ { urls: prefetchPathsOnHover, eagerness: 'moderate', }, ], prerender: [ { urls: prerenderPathsOnHover, eagerness: 'conservative', }, ], };
return ( <Script dangerouslySetInnerHTML={{ __html: `${JSON.stringify(speculationRules)}`, }} type="speculationrules" /> );}
This API is supported by Chrome, Edge, and Opera.
For browsers that don't support the Speculation Rules API, you can fall back to using a link
tag with rel=prefetch
. This API doesn't support prerendering but can be used to prefetch links. To use rel=prefetch
, insert a Link
tag with the link that you would like to prefetch:
<link rel="prefetch" href="/link-to-other-application" />
If using Next.js or React, this can be incorporated into a custom Link tag that can automatically prefetch when the user hovers on the link.
First, set up a context on the page that tracks which links have been prefetched already.
import React, { createContext, useCallback, useEffect, useMemo, useState,} from 'react';
export const PrefetchLinksContext = createContext<PrefetchLinksContext>({ prefetchHref: () => {}, });
export function PrefetchLinksProvider({ children,}) { const [seenHrefs, setSeenHrefs] = useState(new Set()); const [isSafariOrFirefox, setIsSafariOrFirefox] = useState(false);
useEffect(() => { setIsSafariOrFirefox( typeof navigator !== 'undefined' && (navigator.userAgent.includes('Firefox') || (navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome'))), ); }, []);
const prefetchHref = useCallback( (href) => { if (!seenHrefs.has(href)) { setSeenHrefs(new Set(seenHrefs).add(href)); } }, [seenHrefs], );
const value = useMemo(() => ({ prefetchHref }), [prefetchHref]);
if (!isSafariOrFirefox) { return <>{children}</>; }
return ( <PrefetchLinksContext.Provider value={value}> {children} {[...seenHrefs].map((href) => ( <link as="fetch" href={href} key={href} rel="preload" /> ))} </PrefetchLinksContext.Provider> );}
After that, a Link
component can use this context to prefetch when the user hovers.
import { forwardRef, useContext } from 'react';
/** * A Link component that automatically prefetches when visible. */export const Link = ({ children, ...props }) => { const { prefetchHref } = useContext(PrefetchLinksContext); function onHoverPrefetch(): void { if (!props.href) { return; } prefetchHref(props.href); }
return ( <a {...props} onMouseOver={props.prefetch !== false ? onHoverPrefetch : undefined} > {children} </a> ););
The strategy for when to prefetch can also be customized in the component.
Prefetching and prerendering can be effective methods to improve the latency as a user navigates around your application. Both Speculation Rules and rel=prefetch
can be used together to prefetch and/or prerender pages for all browsers.