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

prefetch page context #1617

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open

prefetch page context #1617

wants to merge 41 commits into from

Conversation

usk94
Copy link
Member

@usk94 usk94 commented Apr 22, 2024

resolve #246

This PR introduces a feature to prefetch pageContext (specifically focusing on data) when users hover over a link.

To ensure efficient network usage, the prefetch operation includes a cooldown period to prevent it from being re-triggered too soon after the initial action.
And if the user hovers over multiple links, pageContext fetched for previous links can be used instead of being discarded.

Users can configure this feature through +config.ts or by setting data-prefetch-page-context attributes directly on anchor tags.

@usk94
Copy link
Member Author

usk94 commented Apr 25, 2024

@brillout
I would appreciate some guidance on how to determine when the prefetch of pageContext is complete.
While I have managed to fetch the context containing config, exportsAll, etc., when hovering over a link, I am unsure how to carry this data over to the rendering of the next page after navigation.

Any advice on this would be greatly helpful. Thank you in advance for your assistance.

@brillout
Copy link
Member

@usk94 I will have a look and reply after I'm finished what I'm currently working on. ETA (beginning) of next week.

@usk94
Copy link
Member Author

usk94 commented Apr 28, 2024

@brillout
Thank you for your response; I await your further insights.

@brillout
Copy link
Member

@usk94 Alright, let's do this!

Have a look at renderPageClientSide.ts, that's where the whole client-side rendering orchestration happens. This file has a globalObject so I think we can save the prefetched pageContext there. As for the prefetching itself, how about we use getPageContextFromHooks_isNotHydration() to do the pre-fetching?

Open question: do you think viewport prefetching makes sense for pageContext pre-fetching? This seems quite aggressive, maybe too aggressive. I'd be inclined to not even offer this setting 🤔

@usk94
Copy link
Member Author

usk94 commented May 14, 2024

@brillout

Have a look at renderPageClientSide.ts, that's where the whole client-side rendering orchestration happens. This file has a globalObject so I think we can save the prefetched pageContext there. As for the prefetching itself, how about we use getPageContextFromHooks_isNotHydration() to do the pre-fetching?

Thanks! I'll try it.

Open question: do you think viewport prefetching makes sense for pageContext pre-fetching? This seems quite aggressive, maybe too aggressive. I'd be inclined to not even offer this setting 🤔

Are you concerned that, for example, it might be too aggressive in terms of saturating the network with too many API requests?

For instance, in frameworks like Astro, when viewport prefetching is enabled, the priority of these requests is managed to be lower to mitigate potential network congestion.
https://docs.astro.build/en/guides/prefetch/

But even with that consideration, I agree that it might be too aggressive. I think starting with hover prefetching would be sufficient.

@brillout
Copy link
Member

todo: getPageContextFromHooks_isNotHydration return does not contains "urlOriginal", "Page".
// maybe I need to merge with pageContext(props).

Yes, that's expected. It only fetches the pageContext from hooks, most notably onBeforeRender() and data() (but not onRenderClient()).

I was thinking whether we should also call onRenderClient() upon prefetching but that isn't trivial (it would need to render the next page in some kind of virtual DOM). So I'm thinking that only using getPageContextFromHooks_isNotHydration() is fine for now.

Let me know if you think getPageContextFromHooks_isNotHydration() doesn't fit, I can do some refactoring.

Are you concerned that, for example, it might be too aggressive in terms of saturating the network with too many API requests?

Yes, and also overloading the server (its database and logs).

But even with that consideration, I agree that it might be too aggressive. I think starting with hover prefetching would be sufficient.

👍 We could also, eventually, add support for viewport prefetching for individual links that opt in using the data- attribute but I think we can skip that for now.

Astro

Indeed Astro seems to be using clever heuristics. If you want we can improve Vike's prefetching in a follow up PR after this one. I also have a couple of ideas for improving static asset prefetching.

@brillout
Copy link
Member

@Boeing787 (sponsor who requested this feature) Do you have any special wishes? Your use case is that you want data to be prefetched when the user hovers a on link, correct?

@Boeing787
Copy link

@brillout correct, to load the pageContext

Copy link
Member

@brillout brillout left a comment

Choose a reason for hiding this comment

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

Overall this is going in the right direction 🚀 I made a lot of comments and a couple of new thoughts occurred to me in the process. Let me know what you think. Looking forward to it 👀

@@ -1,5 +1,6 @@
export { prefetch }
export { addLinkPrefetchHandlers }
export { PrefetchedPageContext }
Copy link
Member

Choose a reason for hiding this comment

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

We can use export type { PrefetchedPageContext } to make it a little bit clearer that it's a type.

@@ -48,6 +48,10 @@ const globalObject = getGlobalObject<{
previousPageContext?: { _pageId: string } & PageContextExports
}>('renderPageClientSide.ts', { renderCounter: 0 })

const globalObjectForPrefetchedPageContext = getGlobalObject<{
Copy link
Member

Choose a reason for hiding this comment

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

Let's re-use the globalObject above.

@@ -48,6 +48,10 @@ const globalObject = getGlobalObject<{
previousPageContext?: { _pageId: string } & PageContextExports
}>('renderPageClientSide.ts', { renderCounter: 0 })

const globalObjectForPrefetchedPageContext = getGlobalObject<{
prefetchedPageContext?: PrefetchedPageContext
}>('prefetch.ts', {})
Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see, you basically use getGlobalObject(..., 'prefetch.ts') to share information between modules. Interesting usage.

Or how about we export a new function getPrefetchedPageContext() in prefetchts.ts instead? I've a slight preference for this as it makes the code flow more explicit.

if (
prefetchedPageContext?.pageContextFromHooks &&
'_pageId' in prefetchedPageContext.pageContextFromHooks &&
prefetchedPageContext.pageContextFromHooks._pageId === pageContext._pageId
Copy link
Member

Choose a reason for hiding this comment

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

Instead, how about we make prefetchedPageContext hold one pageContext per pageId?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I'm not sure I understand your point. May I ask for more details?

Copy link
Member

Choose a reason for hiding this comment

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

I think we can save multiple pageContext objects, one per pageId. In other words globalObject.prefetchedPageContext would be a Record<string, PrefetchedPageContext> where string is the pageId. The idea is that if the user hovers over multiple links, the pageContext fetched for previous links can be used instead of being discarded.

Copy link
Member Author

Choose a reason for hiding this comment

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

I understood, thank you!

The idea is that if the user hovers over multiple links, the pageContext fetched for previous links can be used instead of being discarded.

Then, I have implemented this with { pageId: string; prefetchedPageContext: PrefetchedPageContext }[] rather than Record<string, PrefetchedPageContext>(because I struggled to fix type errors in getPageContextFromHooks.ts)

Copy link
Member

Choose a reason for hiding this comment

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

👍 We can polish such small details later.

@@ -203,9 +207,18 @@ async function renderPageClientSide(renderArgs: RenderArgs): Promise<void> {
// Render page view
await renderPageView(pageContext)
} else {
let res: Awaited<ReturnType<typeof getPageContextFromHooks_isNotHydration>>
let res: Awaited<ReturnType<typeof getPageContextFromHooks_isNotHydration>> | PrefetchedPageContext
try {
Copy link
Member

Choose a reason for hiding this comment

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

I thinks we can move the new code outside of the try-catch block?

@@ -109,3 +152,44 @@ function getPrefetchAttribute(linkTag: HTMLElement): PrefetchStaticAssets | null

assert(false)
}

function getPrefetchPageContextAttribute(linkTag: HTMLElement): PrefetchPageContext | null {
const whenAttr = linkTag.getAttribute('data-prefetch-page-context-when')
Copy link
Member

Choose a reason for hiding this comment

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

How about data-prefetch-page-context instead of data-prefetch-page-context-when?

async function prefetchContextIfPossible(url: string, expire: number | undefined): Promise<void> {
const now = Date.now()
const lastPrefetch = globalObject?.lastPrefetchTime?.get(url)
if (lastPrefetch && expire && now - lastPrefetch < expire) {
Copy link
Member

Choose a reason for hiding this comment

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

  • How about some default expire value? E.g. 5 seconds or maybe 30 seconds? I'm not sure.
  • I wonder whether expire should also apply to prefetch() 🤔 WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

I wonder whether expire should also apply to prefetch() 🤔 WDYT?

I think it would be reasonable to exclude the expire logic, as it is expected to be triggered less frequently than hover.

Copy link
Member

Choose a reason for hiding this comment

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

Ok 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

How about some default expire value? E.g. 5 seconds or maybe 30 seconds? I'm not sure.

For now, I set it up in 5 seconds.

return { when: 'hover', expire: parseInt(expireAttr, 10) }
}

assertUsage(false, `data-prefetch-page-context-expire has value "${expireAttr}" but it should instead be number`)
Copy link
Member

Choose a reason for hiding this comment

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

There is some code repetition here. See code above which is a duplicate. This is client-side code so it's important to keep it compact.

if ('prefetchPageContext' in pageContext.exports) {
const { prefetchPageContext } = pageContext.exports

const wrongUsageMsg = `prefetchPageContext should be an object with 'when' and 'expire' properties. 'when' should be false, 'hover', and 'expire' should be a number`
Copy link
Member

Choose a reason for hiding this comment

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

Actually, thinking about it: how about we make the setting simply boolean | number instead?

@brillout
Copy link
Member

As for the failing CI, I think we can have a look at it later.

@usk94
Copy link
Member Author

usk94 commented May 20, 2024

@brillout
Thank you for your review!
Except where I responded to your comments, I have updated based on your feedback.
Could you please check it again?

@usk94 usk94 marked this pull request as ready for review May 21, 2024 07:08
@brillout
Copy link
Member

@brillout Thank you for your review! Except where I responded to your comments, I have updated based on your feedback. Could you please check it again?

👍 I can re-review after we resolved the two pending conversations, but let me know if you want me to re-review before that. Looking forward to it!

@usk94
Copy link
Member Author

usk94 commented May 22, 2024

@brillout
I have updated on all your feedback. May I ask for a re-review?

@usk94 usk94 requested a review from brillout May 22, 2024 14:11
@brillout
Copy link
Member

@usk94 Neat. I will, tentatively today.

@brillout
Copy link
Member

I've started, I'll finish tomorrow.

@brillout
Copy link
Member

b0f2579 fixes the CI.

One (/ the only?) reason is that we need to use the URL instead of _pageId as cache key, because of parameterized routes.

@phonzammi
Copy link
Member

phonzammi commented May 25, 2024

@brillout, Could the issue be that lastPrefetch and'expire might be undefined?

@brillout
Copy link
Member

@phonzammi Maybe, I didn’t dig into that part yet 👀

@usk94
Copy link
Member Author

usk94 commented May 26, 2024

@brillout

One (/ the only?) reason is that we need to use the URL instead of _pageId as cache key, because of parameterized routes.

Thanks! CI passes now.

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

Successfully merging this pull request may close these issues.

Link Prefetching: also prefetch pageContext
4 participants