Faster page transitions with document prefetching
When migrating my blog from Next.js to Remix, I realized that the JavaScript I had was only used to:
- Prefetch pages in the background for faster page transitions
- Lazy load images
Since Remix encourages using Web Standards, HTTP, and HTML, and makes it easy to disable JavaScript, I wanted to recreate these functionalities without the 200k+ of JavaScript that come from React and Remix.
I will cover progressive image loading in another post.
Link prefetching
Link prefetching can be used to prefetch various resources such as CSS and JavaScript. But it can also prefetch the document of the page as follows:
<link rel="prefetch" href="/blog" as="document" />
I use that for my site's primary navigation. If you visit my homepage and open the network tab, you should see the /blog
page prefetched.
From there, when you click on the /blog
link, you should see that it loaded /blog
in 3ms from the prefetch cache.
Double data request
If you're not using a Chromium-based browser, you're probably seeing the document being requested again from server after you click on the link. Which deafets the whole purpose of prefetching.
This is because Chromium-based browsers will cache any prefetched resources for 5 minutes unless the response came with Cache-Control: no-store
. I got this piece of info from Tim Kadlec's post on prefetching and age.
For other browsers that support prefetching, you have to set a browser cache to prevent double data request.
Sergio wrote a great Remix-specific article on how to fix double data request when prefetching in Remix.
Setting max-age=300
in the Cache-Control
will cache both direct requests and prefetched requests for 5 minutes.
Browser cache per route
export const headers: HeadersFunction = () => ({ "Cache-Control": "public, max-age=300, s-maxage=31536000", });
Browser cache for all routes
import type { EntryContext } from '@remix-run/node' import { RemixServer } from '@remix-run/react' import { renderToString } from 'react-dom/server' export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext ) { let markup = renderToString(<RemixServer context={remixContext} url={request.url} />) responseHeaders.set('Content-Type', 'text/html') responseHeaders.set('Cache-Control': 'public, max-age=300, s-maxage=31536000') return new Response('<!DOCTYPE html>' + markup, { status: responseStatusCode, headers: responseHeaders, }) }
Browser cache for prefetch requests only
import type { EntryContext } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; import { renderToString } from "react-dom/server"; export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext ) { let markup = renderToString( <RemixServer context={remixContext} url={request.url} /> ); responseHeaders.set("Content-Type", "text/html"); let purpose = request.headers.get("Purpose") || // chrome, safari, and edge request.headers.get("X-Purpose") || // old chrome request.headers.get("Sec-Purpose") || // future chrome request.headers.get("Sec-Fetch-Purpose") || // future chrome request.headers.get("X-Moz"); // firefox let isPrefetch = purpose === "prefetch"; responseHeaders.set( "Cache-Control", `public, max-age=${isPrefetch ? 300 : 0}, s-maxage=31536000` ); return new Response("<!DOCTYPE html>" + markup, { status: responseStatusCode, headers: responseHeaders, }); }
Prefetch on hover or focus
If you have a huge list of blog posts, you might not want to prefetch all of them. In such case, you can prefetch the first couple of posts normally, and only prefetch the rest if the user hover or focus on them.
For that, I use the below snippet <script dangerouslySetInnerHTML>
.
const addPrefetchLinkAfterRelativeLink = (event) => { const href = event.target.getAttribute("href"); const prefetchLink = document.querySelector( "link[as='document'][href='" + href + "']" ); if (prefetchLink || href === window.location.pathname) { return; } const link = document.createElement("link"); link.setAttribute("rel", "prefetch"); link.setAttribute("href", href); link.setAttribute("as", "document"); event.target.after(link); }; document.addEventListener("DOMContentLoaded", () => { const relativeLinkNodes = document.querySelectorAll("a[href^='/']"); relativeLinkNodes.forEach((node) => { node.addEventListener("focus", addPrefetchLinkAfterRelativeLink); node.addEventListener("mouseenter", addPrefetchLinkAfterRelativeLink); node.addEventListener("touchstart", addPrefetchLinkAfterRelativeLink, { passive: true, }); }); });