By

Page prefetching and transitions in Astro

There’s a middle ground between MPA and SPA.

Astro excels at building multi-page apps, where each navigation triggers a full page reload, fetching the new statically generated page and displaying it. This is how the web used to work originally until fancier approaches like single-page apps came along, but at the cost of metrics like bundle size which impacts performance negatively. It could be worth it, for instance if the app is heavily interaction based, like a music player or calendar app. For content-focused sites like blogs however, you'd want to prioritize faster initial loads and probably static site generation or server side generation.

However, while multi-page apps definitely score higher in page metrics and load the first page faster, they feel terribly slow to use. Each link press is, no matter how fast your internet is, followed by a noticeable delay where the next page is fetched and javascript reloaded. I found that this site felt especially sluggish, and to solve it I would have to cross into the hybrid realm between SPA and MPA.

The technique

How? Glad you asked!

The short answer is swup.js.

The long answer is a technique based around using javascript to prefetch pages on hover, and replace the current page’s content with the new page’s content, of course while incorporating some fancy animations during the switch. This strategy initially arrived in the form of PJAX, which is a combination of the words pushState and AJAX, back in 2012. Fear not, you don't have to use a library from 2012 to get it working. Today there are multiple libraries leveraging the technique in modern ways like Taxi.js, Fireship's flamethrower and the one I ended up going with, swup.js.

I need to stress however that this is no silver bullet. All of these alternatives add varying amounts of kb to the bundle size, a problem Astro is supposed to defeat. The sizes are however much lower than fully fledged frameworks like Next.js, Vite and similar alternatives. It’s a middle ground that I think strikes a good balance of performance and UX.

Setup

To set it up, it’s quite simple really. To recreate the setup used on this page, you first want to install swup and its plugins:

pnpm add -D swup @swup/a11y-plugin @swup/head-plugin @swup/slide-theme @swup/preload-plugin @swup/progress-plugin @swup/scroll-plugin

In your page/layout component (whichever one is present on all pages and contains a slot), you’d wanna do something like this:

<script>
  import Swup from "swup";
  import SwupA11yPlugin from "@swup/a11y-plugin";
  import SwupHeadPlugin from "@swup/head-plugin";
  import SwupSlideTheme from "@swup/slide-theme";
  import SwupPreloadPlugin from "@swup/preload-plugin";
  import SwupProgressPlugin from "@swup/progress-plugin";
  import SwupScrollPlugin from "@swup/scroll-plugin";

  const swup = new Swup({
    plugins: [
      new SwupA11yPlugin(),
      new SwupHeadPlugin(),
      new SwupSlideTheme(),
      new SwupPreloadPlugin(),
      new SwupProgressPlugin(),
      new SwupScrollPlugin(),
    ],
  });
</script>
  
<html lang="en">
  <head>
    <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
  </head>
  <body class="page">
    <div class="page-container">
      <Header client:load activeTag={title} />
      <main id="swup" class="wrapper"> // Important! Add id="swup"
        <slot />
      </main>
    </div>
    <Footer />
  </body>
</html>

Page.astro

And that should be all to get the effect working. In this configuration, links are prefetched only when hovered to prevent full SPA behavior, while the content of the page is animated with a slide+fade animation. How you handle your navigation bar is up to you, but I chose to listen for swup’s lifecycle methods and that way update which link is highlighted in my navigation bar react component. You can for instance do something like this:

useEffect(() => {
    const handleRouteChange = () => {
      // Do something
    };
    document.addEventListener("swup:pageView", handleRouteChange);
    return () => {
      document.removeEventListener("swup:pageView", handleRouteChange);
    };
  });

Conclusion

Until more standard page transitions drop, like the View Transitions API for multi-page apps, this is decent compromise that significantly enhances the feel of a site, at the cost of an increase in bundle size. I find responsiveness and animations important and will definitely keep using similar techniques if necessary when working on multi-page apps, and perhaps Astro will add something similar natively in the future.