Auto Prefetch data

OPENING POST


Description

Right now prefetching has to be done manually by the theme developer. For example, when a post loads, it can use useEffect to prefetch the home.

We can add an autoPrefetch feature to the <Link> component so it does so automatically.

User Stories

As a Frontity developer
I want to auto prefetch the data of all the links that exist in the DOM
so that when they click, the route change is instant

As a Frontity developer
I want to auto prefetch the data when a readers hover on a link
so that when they click, the route change is instant

As a Frontity developer
I want to auto prefetch data of the links on the readers’ screen
so that when they click, the route change is instant

Possible solution

It could have four modes:

  • "no": it doesn’t prefetch
  • "hover": it does prefetching when the reader hovers over the link.
  • "in-view": it does prefetching when the link enters the screen.
  • "all": it does prefetching when the link component is mounted.

That setting would have to be exposed for the final theme user. Maybe it could be in:

  • state.theme.autoPrefetch.
  • state.router.autoPrefetch (only if the Link component ends up in libraries.router).
  • state.components.linkPrefetch.

SUMMARY


:warning: Please, bear in mind that this section wasn’t part of the opening post. It has been added afterwards and its purpose is to keep a quick summary with the most relevant information about this FD at the top of the thread.

Relevant links

We can disable the feature if the connection is slow, like the flying-pages library is doing: https://github.com/gijo-varghese/flying-pages/blob/master/flying-pages.js

We can also use mouseout and touchstart for the "hover" option.

I think that maybe, instead of doing the real prefetch with actions.source.fetch, we should add a <link prefetch src=""> with the URL of the API, so this prefetches don’t impact the performance.

We could do that with a new action, actions.source.prefetch. That way, when we do the route change and do the actual actions.source.fetch, the information is already there and it only has to process it (add it to the state and so on). If we do this, we avoid adding things to the state that are not needed yet.

1 Like

I think we should also be able to disable prefetching at the Link level.

Something like <Link prefetch={false}>Link</>

As a Frontity developer
I want to be able to disable prefetching for specific links
so that I have granular control of which links are worth prefetching

This article is an interesting read: https://web.dev/route-prefetching-in-nextjs/

Just created a very basic draft PR. But it already has “all” and “in-view” mode working.

1 Like

One benefit of actually running fetch and putting things on state is because we know exactly what we need to fetch, as an example, once we prefetch a category route we get back a list of posts. If we have a link to a post from that category in the current page we won’t need to prefetch again, in my PR I’m relying on state.source.get to decide whether the link needs prefetching or not.

What do you think about prefetching in batches instead of using <link prefetch />? We could limit the max number of prefetches.

On another note any suggestion for mocking intersection observer and testing the in-view mode?

To be honest, I don’t like the idea of using <link prefetch > that much either.

As you said, it’s very convenient to have that data in the state as soon as possible, and on the other hand it won’t work with requests that require an additional fetch, like for example getting the category ID when only the category slug is known to then fetch the posts from that category.

I prefer your idea of prefetching in batches. To avoid a performance hit on the Time to Interactive metric, maybe we could use requestIdleCallback instead of a setTimeout. It is now supported on all major browsers except Safari. That way, this should not affect the LightHouse score at all.

What do you think?

We could also use requestIdleCallback, or even React’s new scheduler package, to populate the state once the data has been returned from the REST API. I’ll study that for the v2 of source.

A while ago I thought about that as well and I think the client logic to accomplish that would be too complex. There are two options though:

  1. Create the @frontity/wpgraphl-package, which is something we want to do, and make sure that we combine the requests. I’m not an expert on GraphQL but my understanding is that it should be because GraphQL supports multiple queries in the same request.

    This is something we want to after we release Source v2. The main drawback is that setting up a cache for GraphQL is way more complex than for a REST API.

  2. Create a new REST API endpoint that supports multiple requests.

    This is more of an idea for the future: create a @frontity/frontity-source package that uses the REST API but with a custom endpoint, like /wp-json/frontity/v1. That endpoint could have these features:

    • Get any entity only using the URL, to avoid the case where we now have to multiple requests (the example of the category I referred to earlier).
    • Accept multiple requests.
    • Merge all embeds (authors, categories, tags, media) together to avoid repetition.

    Something like this:

    https://mywp.com/wp-json/frontity/v1/entities?url[]=/category/one&url[]=/category/two
    

    Could return:

    {
      "/category/one": [
        // Posts of /category/one...
      ],
      "/category/two": [
        // Posts of /category/two...
      ],
      "embeds": {
        // All the embeds together...
      }
    }
    

    Using the URLs in the REST API request will also be very convenient to do instant caching invalidations, because if a caching plugin is telling the CDN to purge the /category/one URL, it will also purge the REST API requests that contained it, like /entities?url[]=/category/one. I know it’s probably not that simple, but at least it could be more similar to the way WordPress plugins invalidate the cache nowadays.

But of course, it’ll take a while until we can do something like that :slightly_smiling_face:

I agree, I’ll look into these options and update the PR :+1:

I agree it is much more complex than I initially thought. And I think if we’re going to officially support a wpgraphql-package it does not make sense to do that when using the rest-api as wpgraphql solves that by design.

I don’t really like the idea of creating and maintaining a new rest API endpoint, we have done that to solve similar issues at 10up but we found ourselves spending too much time maintaining an unofficial implementation and there’s also the fact that frontity wouldn’t just work out of the box with WordPress. If people using frontity care enough about these issues they should be able to switch to GraphQL instead which already has a well-established plugin.

1 Like

This has now been merged in: https://github.com/frontity/frontity/pull/518 thanks to @nicholasio.oliveira :slightly_smiling_face:

There are four supported modes:

  • “in-view”: Only prefetch links that are currently visible in the viewport. (recommended)
  • “all”: Prefetches all internal links on the page.
  • “hover”: Prefetches links on hover.
  • “no”: No auto prefetch.

The prefetch mode should be set in state.theme.autoPrefetch.

As mentioned in the PR, we haven’t added the requestIdleCallback optimization at this point, but we can explore that in the future:

There are some other things for us to consider like adding a shim for it. TypeScript also does not recognize requestIdleCallback as it is officially experimental so we would have to figure that out as well. It doesn’t feel right to add a shim and typescript definitions for this in the Link component so I’d rather do that a part of a new FD.

This feature has been released :tada: : Read the full Release Announcement.

Here we have a quick demo of it, how to configure and how it works:

4 Likes

2 posts were split to a new topic: Problems with the auto-prefetch feature

@nicholasio.oliveira while investigating a TypeScript issue when we merge our package types, I noticed that the <Link> component didn’t have any types, so I added them in this PR, in case you want to take a look. They are similar to the types use for packages, we import the types of all the packages the <Link> component needs to know about.

I also moved the component into three files because it was getting a bit too big, and I refactored the process queue to a class. I hope you don’t mind :sweat_smile:

@luisherranz Looks good to me! (Just left some comments in the PR). Thanks for the ping!

1 Like