Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure Astro prefetch behaviour #201

Open
jbmoelker opened this issue Nov 4, 2024 · 2 comments
Open

Configure Astro prefetch behaviour #201

jbmoelker opened this issue Nov 4, 2024 · 2 comments
Labels
enhancement New feature or request performance

Comments

@jbmoelker
Copy link
Member

User stories

As a website visitor,
I want web pages to be prefetched,
so that navigation is instant when I hit a link.

As a website editor,
I want web pages to be cached only shortly,
so that the changes I make propagate reasonably quickly.

Approach

Use Astro's prefetch feature.

Questions

  • Which default strategy should we use (hover, tap, viewport, load)?
  • Do we add caching headers for better Firefox and Safari support? Cloudflare doesn't provide ETags, but we could add short Cache-Control and Expires headers either through public/_headers config or in a Service Worker (see Add Service Worker #199). Through a Service Worker we could even add our own ETags 🤔.
  • If our default prefetching is conservative (hover or tap), should we add inline hints [data-astro-prefetch="load"] to the main menu items (see Editable site menu #196)?
@decrek
Copy link
Member

decrek commented Jan 2, 2025

Personally I always had my doubts with prefetching of pages. I think it comes from large bundle splitted client(or hybrid) side rendered JS heavy apps where you actually run into performance issues on lower end CPU devices that struggle with parsing and executing JS. I even think that caching and prefetching in some setups actually include the parsing of JS. So I think it stems from (partially) solving an actual performance problem.

In our setup and context I have these considerations:

  • It would be nice, but the problem being solved isnt that big. static HTML pages are not slow, server rendered pages could involve some logic making it a bit slower, but this is (most of the time) also solvable with some caching headers.
  • It will definitely lead to overprefetching of pages, I dont like that, but on the other hand, how bad is that really.
  • The fact that cloudlfare doesnt support etags makes it more complicated, thus the proposals to add caching headers or even a serviceworker in order to make this actually viable.

My initial humble opinion is that we need to add a lot of complexity in order to have little benefits, while overfetching.

But to answer your questions:

Which default strategy should we use (hover, tap, viewport, load)?

As a default strategy I would stick to the default of hover, imo this has the best balance between actually determining wether someone is going to visit a page and not overfetching.

Do we add caching headers for better Firefox and Safari support? Cloudflare doesn't provide ETags, but we could add short Cache-Control and Expires headers either through public/_headers config or in a Service Worker (see #199). Through a Service Worker we could even add our own ETags 🤔.

  • We need to set caching headers in order for this to work cross browser and I think it makes sense for dynamic pages to do this anyways, but this depends per page if you actually want to set it. So here the question becomes wether we want to set caching headers for all HTML pages in head start, because you dont always want that and so it could become confusing.
  • Adding a service worker is a separate question, but I understand the etag implementation from the scope of this ticket, so ill just address that part. I always feel like that (for every HTML request) reading the response body, generating a md5 hash and adding that as a (custom) header adds overhead to every HTML request, which is something that I tend to avoid as much as possible. Here we have to take into account that the etag magic comes from the "if non match" request header, where the server decides to not send the body of the response if the etag hasnt changed. If we implement etags in a serviceworker in order to do the comparison you need to get the "fresh" response body meaning we already downloaded the response body, meaning there is nothing to optimize anymore.

If our default prefetching is conservative (hover or tap), should we add inline hints [data-astro-prefetch="load"] to the main menu items (see #196)?

No I dont think so :) I think the prefetching on hover will also optimize these links and adding [data-astro-prefetch="load"] will just lead to more overfetching.

@jbmoelker
Copy link
Member Author

Okay, agreed we can limit default behaviour to hover only and mention more aggressive prefetching in the docs.

With #242 nearly merged, we could use that setup to add an addEtagPlugin to the page route handler:

// Custom plugin to add ETag headers if missing
const addEtagPlugin = {
  cacheWillUpdate: async ({ response }) => {
    if (!response) return null;
    
    const clonedResponse = response.clone();
    
    // If response already has an ETag, return it unchanged
    if (clonedResponse.headers.has('etag')) {
      return clonedResponse;
    }
    
    // Generate ETag from response content
    const content = await clonedResponse.clone().text();
    const hash = await crypto.subtle.digest(
      'SHA-1',
      new TextEncoder().encode(content)
    );
    const etag = Array.from(new Uint8Array(hash))
      .map(b => b.toString(16).padStart(2, '0'))
      .join('');
    
    // Create new response with ETag header
    const newHeaders = new Headers(clonedResponse.headers);
    newHeaders.set('etag', `"${etag}"`);

    return new Response(content, {
      status: clonedResponse.status,
      statusText: clonedResponse.statusText,
      headers: newHeaders,
    });
  }
};

registerRoute(
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    networkTimeoutSeconds: 3,
    cacheName: 'pages',
    plugins: [
      addEtagPlugin,
      new CacheableResponsePlugin({
        statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxEntries: 30,
      }),
    ],
  })
);

@decrek what do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request performance
Projects
None yet
Development

No branches or pull requests

2 participants