Add an option to prefetch .link navigation

Many web frameworks (e.g. Remix, Gatsby) have an option for their link components that begins the navigation request on hover so that when the link is actually clicked ~300ms later the page is cached and ready to be swapped over. This compensates for network latency very effectively, my ping from Sydney to Frankfurt is about 280ms.

This would be an enourmous improvement in UX for people not geographically close to servers, and as nice as it would be to put everything on fly.io or some other edge equivalent it’s not an option for many people

My understanding of the challenges involved is that the biggest issue would be desync between what the server thinks the DOM looks like vs what is actually there if the link is hovered over but not actually clicked.

I would work on this myself but i don’t feel like my knowledge of liveview is adequate to implement this without creating a huge mess :slight_smile:

7 Likes

This might be a huge rabbit hole. Think of mount callbacks connecting to presence, pubsub or the likes triggering side effects beyond what you really intend on a prefetch. You might also need to be able to run two LV processes side by side unless you dig in real deep and remodel probably half of LV to add a new specialized prefetch rendering mode.

I would love to see this. It’s so common nowadays, it’s a miss on Phoenix not to have something like that OOTB.

Btw I kind of hackily did this (don’t run this in production, probably) like so:

function prefetchPage(url) {
  // Create a hidden iframe to load the page
  const iframe = document.createElement("iframe");
  iframe.style.display = "none";
  iframe.src = url;
  document.body.appendChild(iframe);

  // Remove the iframe after loading to clean up
  iframe.onload = () => {
    setTimeout(() => {
      document.body.removeChild(iframe);
    }, 1000); // Give some time for resources to load
  };
}

document.querySelectorAll("a").forEach((link) => {
  let timer;

  link.addEventListener("mouseenter", () => {
    // Start a timer to avoid prefetching on quick hover-bys
    timer = setTimeout(() => {
      prefetchPage(link.href);
    }, 100);
  });

  link.addEventListener("mouseleave", () => {
    // Cancel prefetch if mouse leaves before timer completes
    clearTimeout(timer);
  });
});
1 Like

I would recommend using fetch with priority: low instead of embedding an iframe to reduce memory, not modify the DOM, etc. I’d also recommend using mouseover and mouseout, and further would recommend pointerover and pointerout instead to support more pointing devices. For both of those, obviously you want to make sure the changed behavior is acceptable/handled correctly. I also think the 100ms delay is kind of counterproductive for this use, but I get it. Using fetch instead also allows you to easily abort the request if desired.

You also asked about this behavior in relation to Next on Slack, which does the hover logic when prefetch=false. For prefetch=true, which fetches the linked page when it’s in the viewport, this can be done pretty easily with the Intersection Observer API.

As far as the interaction with liveview, I don’t have anything useful to add, unfortunately.

1 Like

Been looking at this thread and thinking…

I think the problem with fetch (and the iframe) in the case of LiveView is that it probably doesn’t help make the live navigation (over the persistent WebSocket connection) any faster, and brings questions about behavior.

JS fetch would probably cause only a “disconnected mount” of the prefetched page/LiveView, while the iframe would trigger both disconnected and connected mounts with a full LiveView process on the server which can’t really be reused when the actual navigation happens.

2 Likes

Yeah, I think it would only help when using <.link href=foo>, which does an HTTP request, as far as I understand. For live navigation, I don’t know enough to say if the familiar “prefetch” tricks are useful, but I wouldn’t expect them to be.

1 Like

I think the approach that astro does is the cleanest and mostly leveraging browser standards:

  1. It prefers using speculation rules, if available
  2. It falls back to rel=prefetch, if available
  3. finally, resorts to using fetch()

They also keep track of already prefetched URLs in a set and bail early.

Prefetching can be configured to be global on the project, opt-in/out per link, etc. Very good ergonomics!

2 Likes

Unfortunately the only option of the three that is actually available in >80% of user agents is fetch. This may not matter for certain purposes (if you know or are able to require your users to use Chrome, mainly), but I’m personally not a fan of using Chrome-only features (rel=prefetch is not Chrome-only, for what it’s worth, just not as widely available as fetch).

Anyway, still not sure how relevant any of this is to liveview :slight_smile:

wdym? this works on chrome/chromium, safari, firefox? Plus most of the world is on Chrome. Would you really not ship an optimization for the majority of the world because it’s chrome-only? Sounds like a terrible trade-off!

I looked at some of the Astro docs to understand/remember what it is all about: Why Astro? | Docs

IIUC, Astro allows one to build server-rendered static pages. Navigation then means full/regular HTTPS requests.

External links are not prefetched, so prefetching focuses on optimizing internal page navigation.

LiveView’s focuses on a different set of goals. Whereas Astro contrasts itself with other JS frameworks primarily targeted at building web applications (with login, dashboards, User Interactions, etc), LiveView is a tool for building just that, albeit in a different way.

LiveView has a persistent WebSocket connection and internal navigation over the WebSocket bypasses most of what goes on in an HTTPS request. I think Astro doesn’t have that.

Typical LiveView applications have state, and that complicates the applicability of prefetching as others have mentioned.

At this point I don’t know what are we actually trying to improve in LiveView with prefetching, or is it just trying to copy a solution to a problem we don’t have :slight_smile:

2 Likes

Cross-site navigation in phoenix projects (liveview or not) is just slower compared to astro, nextjs, remix etc. All of them to prefetch on hover (or when a link is in the view port). This means fetching images etc. So navigating to another page within the site is instant.

If you think Phoenix is ok staying behind as the “slow” solution (e.g. try using hexdocs sidenav- it is def not instant), then fine :slight_smile:

1 Like

Yes but that state is made of immutable data. Computing a new state doesn’t invalidate it. It’s more a question of not doing update in the database, using pubsub on so on.

So we can’t turn it on by default for all views but maybe it can be opt it?

Hexdocs is not liveview so it is a bad example to use for “liveview slow”.

Besides, I struggle with the “slow” argument. If your page rendering is 10ms you are looking at 10 ms + latency should, can be anything from 5 to 50ms.

So in total, you are looking at 15-60ms from the moment you click. I can’t see a reason why I would get into huge technical complexity to reduce that.

1 Like

Prefetch in Next, Remix, etc. is generally applied on SSG content, not SSR AFAIK.

As mentioned above, a prefetched Liveview should not trigger any side effect and it may be hard to track. And the prefetch result should be invalidated at every state modification, I suppose.

However, I can imagine a world where, if every assign of the Liveview is async and rendered insided .async_result, we might be able to load the static part of the DOM with the .async_result slots in a loading state. It might then be patched by classic Liveview mechanisms after an effective navigation.

In such a world, even mount should not run on prefetch to prevent any side effect. A prefetch would basically return the template with every .async_result in a loading state. Maybe we could imagine a prefetch_default option on assign_async/4 if it is useful (right now I don’t find any use for it, even for skeleton purposes).

P.S: maybe that prefetch_default would be more useful on sync assigns to consider it “prefetch compatible” even if it not async.

Sure, 15-60ms is fine, and if you’re in the US or to a lesser extent europe that is probably what you’re going to get.

Unfortunately, a lot of apps cannot have distributed servers so the network round trip latency is going to have a much wider range than that. For example, at my company the servers must be in the EU but we have a significant percentage of the userbase in Australia. That means an absolute bare minimum of 400ms for half of our users which is a very bad experience. I’d love to use LiveView for this because I really prefer the DX but realistically this is forcing us to do an SPA because half a second for an page transition is just bad

There is no free lunch. Fetching the actual data will take 300ms regardless, wether you use an SPA or Liveview.

Liveview does provide some utilities to “fake” response UI stuff. Things like clicking a button and showing a spinner or a loading class on the button while you fetch the next view.

Do not underestimate the amount of money it costs to keep an SPA up to date. Just the maintenance is absolute hell.

1 Like