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

pre populate cache with views based on links with turbo_preload attribute #552

Merged
merged 16 commits into from
Jun 18, 2022

Conversation

hey-leon
Copy link
Contributor

@hey-leon hey-leon commented Mar 17, 2022

First off, we ❤️ developing with hotwire and turbo at my workplace. So I want to thank all the contributors and the team.

Description

At my workplace we have released a new mobile application using hotwire and coming from a react native application one part of the UX that felt a bit less refined was the first 0-300ms of navigation. When navigating to a page for a second time turbo loads a snapshot from cache which makes for a pretty snappy (pun intended) experience. However for hot paths we wanted to make this the reality on first visits.

Prior Art

Before investigating this solution we did look at leveraging browser based prefetching. This can be achieved via the usage of <link rel="prefetch" href="/app/path/here" />. However our experience showed a couple ergonomic challenges using this for app screens:

  • It does not appear to follow redirects in major browsers (expectations do not seem to be defined in specs).
  • It relies on usage of the Cache-control header.
    • To leverage it you essentially need to completely hand over cache control to the browser which is not ideal for highly dynamic web apps.

The implementation in this PR makes it possible to pre populate the cache by marking links with data-turbo-preload. which makes for better user experience without needing to worry about stale views.

@hey-leon hey-leon changed the title preload cache with views based on links with new turbo-preload attribute pre populate cache with views based on links with new turbo-preload attribute Mar 17, 2022
@seanpdoyle
Copy link
Contributor

seanpdoyle commented Mar 17, 2022

Thank you for opening this PR, the introduction of the Preloader class has a lot of potential!

I wonder, had you considered reading from an <a> element's [rel] attribute, instead of introducing a bespoke [data-turbo-preload] attribute?

There are existing semantics for values like rel="prefetch" and rel="preload", which could pose opportunities for progressive enhancement while gracefully degrading to built-in browser support when JS is disabled or Turbo fails to load.

}
}

preloadURL(link: Element) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this method accepted HTMLAnchorElement as a type, we could read the link.href directly, without the need to call expandURL or || "".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback @seanpdoyle I'll have a go at refining the types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.then(res => res.text())
.then(responseText => PageSnapshot.fromHTMLString(responseText))
.then(snapshot => this.snapshotCache.put(url, snapshot))
.catch(() => {})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This codebase utilizes async/await syntax, which enables this to be implemented without chaining:

const response = await fetch(url.toString(), { headers: { 'VND.PREFETCH': 'true', 'Accept': 'text/html' } })
const responseText = await response.text()
const snapshot = PageSnapshot.fromHTMLString(responseText)

this.snapshotCache.put(url, snapshot)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that does seem to be the convention. I will tidy this up 👍. Thank you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hey-leon
Copy link
Contributor Author

hey-leon commented Mar 17, 2022

Thank you for opening this PR, the introduction of the Preloader class has a lot of potential!

I wonder, had you considered reading from an <a> element's [rel] attribute, instead of introducing a bespoke [data-turbo-preload] attribute?

There are existing semantics for values like rel="prefetch" and rel="preload", which could pose opportunities for progressive enhancement while gracefully degrading to built-in browser support when JS is disabled or Turbo fails to load.

Good point

This is a great thought. The "prefetch" and "preload" types are not currently supported by anchor tags (link only) at this point. See this table for rel type support. But based on moz docs it appears it is at least being considered here.

What is your thoughts on adopting this pre-emptively. I am open to the refactor if we think this is a good approach?

EDIT: I have gone ahead and updated the PR to use a[rel="preload|prefetch"]

@@ -28,23 +27,25 @@ export class Preloader {
const links = element.querySelectorAll('a[data-turbo-preload="load"]')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does executing this query with additional type information help avoid the instances check?

for (const link of element.querySelectorAll<HTMLAnchorElement>('a[data-turbo-preload="load"]')) {
  this.preloadURL(link)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 15fedf0

@hey-leon hey-leon changed the title pre populate cache with views based on links with new turbo-preload attribute pre populate cache with views based on links with rel="preload|prefetch" attribute Mar 17, 2022
@rik
Copy link
Contributor

rik commented Mar 18, 2022

I'm concerned about Turbo re-purposing existing HTML attributes. Currently, rel=preload and rel=prefetch aren't allowed on <a> by the HTML spec. If the HTML spec wants to change to allow it, Turbo might get in the way.

On top of that, I'm not sure this feature is needed in Turbo. You can add <link rel=prefetch> in your HTML (or in JS) and supporting browsers will prefetch them. So the fetch call from Turbo when visiting should be instant. Maybe just a polyfill for Safari until they implement prefetching?

@hey-leon
Copy link
Contributor Author

hey-leon commented Mar 18, 2022

TLDR – Preloading a preview gives you instant feel but you still load the most up to date content giving much better ergonomics!

I'm concerned about Turbo re-purposing existing HTML attributes. Currently, rel=preload and rel=prefetch aren't allowed on <a> by the HTML spec. If the HTML spec wants to change to allow it, Turbo might get in the way.

@rik you are correct that they are not supported on anchors at this point. However mozilla's documentation demonstrates an openness to to accept it. Aside from this, the more I get in the weeds on this PR I am realizing there are some fundamental differences between the changes in this PR and link[rel="prefetch"] and I think that is probably a good enough reason to change to a custom attributes.

On top of that, I'm not sure this feature is needed in Turbo. You can add <link rel=prefetch> in your HTML (or in JS) and supporting browsers will prefetch them. So the fetch call from Turbo when visiting should be instant. Maybe just a polyfill for Safari until they implement prefetching?

After my exploration I am not sure if link[rel="prefetch"] is an awesome time for the use case outlined in this PR. Leveraging link[rel="prefetch"] was the first approach I looked at before authoring this PR. I too would prefer to leverage the native browser functionality. However the ergonomics seem to be quite challenging. There are two main problems:

  • It doesn't appear to follow redirects (however this seems to be undefined in spec)
  • It sensibly but painfully abides the cache-control header.
    • i.e It makes sense for static assets but not a whole lot for highly dynamic content.

Basically the only way to use link[rel="prefetch"] is to set the cache control header to say e.g "Hey you can cache that for 30 minutes". Which could be quite challenging with highly dynamic pages because the options really boil down to:

  • Set it very short and possibly have it expire.
  • Set it high and possibly have it be stale.

@hey-leon hey-leon changed the title pre populate cache with views based on links with rel="preload|prefetch" attribute pre populate cache with views based on links with turbo_preload attribute Mar 18, 2022

export class Preloader {
readonly delegate: PreloaderDelegate
readonly selector: string = 'a[data-turbo-preload="true"]'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
readonly selector: string = 'a[data-turbo-preload="true"]'
readonly selector: string = 'a[data-turbo-preload]'

I think this is the preferred style here when a truthy attribute is expected, for example see

return [ ...this.element.querySelectorAll("[id][data-turbo-permanent]") ]

@hey-leon hey-leon force-pushed the feature/preload-onload branch from 9e3b3fc to 520a132 Compare March 31, 2022 22:38
@hey-leon hey-leon force-pushed the feature/preload-onload branch from 520a132 to f2b94bd Compare May 4, 2022 20:48
@hey-leon
Copy link
Contributor Author

hey-leon commented May 4, 2022

Hey turbo team, I wanted to circle back here as I have not stayed on top of this.

I can say that we have been using this in production to preload our hottest app paths with success. I am wondering if there is interest in merging this feature or a revision of this?

@seanpdoyle do you have any opinions?

@ghiculescu
Copy link

@dhh do you have any feedback on the concept here?

@silentjay
Copy link

Hoping this gets merged. Some of us have already implemented pre-fetching via 3rd party scripts mentioned in this old issue #174 would be great to have it built in.

@dhh
Copy link
Member

dhh commented Jun 18, 2022

This is great. Agree it would be nice to lean on a href's support when that's there, but don't think we should get ahead of the spec. If it ends up happening, it wouldn't be a big deal to go from data-turbo-preload to whatever that is. But as with the link preloads, it might well be that the cache-control dynamics don't match with what we're trying to do here.

@dhh dhh merged commit acf23e8 into hotwired:main Jun 18, 2022
@ghiculescu
Copy link

Should we update https://github.com/hotwired/turbo-site to note this feature? Maybe as a new item in https://turbo.hotwired.dev/handbook/drive ?

@dhh
Copy link
Member

dhh commented Jun 20, 2022

Yes please do add a PR we can ship with 7.2.0 👍

@hey-leon
Copy link
Contributor Author

hey-leon commented Jun 20, 2022

This is great. Agree it would be nice to lean on a href's support when that's there, but don't think we should get ahead of the spec. If it ends up happening, it wouldn't be a big deal to go from data-turbo-preload to whatever that is. But as with the link preloads, it might well be that the cache-control dynamics don't match with what we're trying to do here.

Thanks for taking the time to understand the PR @dhh. I think your analysis pretty spot on as well.

Should we update https://github.com/hotwired/turbo-site to note this feature? Maybe as a new item in https://turbo.hotwired.dev/handbook/drive ?

Yes please do add a PR we can ship with 7.2.0 👍

Good suggestion. I will put something together 👍

@clementmas
Copy link

Are the linked pages all preloaded on page load or only when each link becomes visible?

If not, I think that could be a great addition in a new PR. Using Intersection Observers. I think Next.js and Nuxt.js do that automatically.

@hey-leon
Copy link
Contributor Author

Are the linked pages all preloaded on page load or only when each link becomes visible?

If not, I think that could be a great addition in a new PR. Using Intersection Observers. I think Next.js and Nuxt.js do that automatically.

That could be a pretty cool idea @clementmas

@pfeiffer
Copy link
Contributor

Are the linked pages all preloaded on page load or only when each link becomes visible?
If not, I think that could be a great addition in a new PR. Using Intersection Observers. I think Next.js and Nuxt.js do that automatically.

That could be a pretty cool idea @clementmas

In that case I'd prefer to have links annotated with data-turbo-preload="lazy" to opt-in to the lazy-preloading behavior. I believe that links explicitly hinted with data-turbo-preload="true" should still be preloaded, even outside of the viewport, as those links could be visited programatically.

A different approach to this could be to implement the functionality in this PR using MutationObserver instead of preloading links after a frame visit or on document load. This would allow developers to define their own behavior on top, eg. by implementing an Interaction Observer that sets the data-turbo-preload as links scroll into view.

It could also be used to add data-turbo-preload when the user has a "hover intent" (ie. like https://www.npmjs.com/package/hoverintent).

@hey-leon
Copy link
Contributor Author

hey-leon commented Aug 17, 2022

Are the linked pages all preloaded on page load or only when each link becomes visible?
If not, I think that could be a great addition in a new PR. Using Intersection Observers. I think Next.js and Nuxt.js do that automatically.

That could be a pretty cool idea @clementmas

In that case I'd prefer to have links annotated with data-turbo-preload="lazy" to opt-in to the lazy-preloading behavior. I believe that links explicitly hinted with data-turbo-preload="true" should still be preloaded, even outside of the viewport, as those links could be visited programatically.

A different approach to this could be to implement the functionality in this PR using MutationObserver instead of preloading links after a frame visit or on document load. This would allow developers to define their own behavior on top, eg. by implementing an Interaction Observer that sets the data-turbo-preload as links scroll into view.

It could also be used to add data-turbo-preload when the user has a "hover intent" (ie. like https://www.npmjs.com/package/hoverintent).

Yes I don't think there is any reason to remove the current functionality for our purpose I'm not sure we would need the intersection observer but I cannot see why we can't have both approaches. Having said that if we wanted to keep the API simple I don't see any downside to the intersection observer approach for us. E.g. most things we are preloading are defined in fixed navbars and they usually load in such a small time frame that the time between the link coming into frame, and pressing it, it would likely have already been loaded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

8 participants