vikejs / vike

🔨 Flexible, lean, community-driven, dependable, fast Vite-based frontend framework.
https://vike.dev
MIT License
4.1k stars 346 forks source link

Client-side Routing without hitting the server #95

Closed brillout closed 2 years ago

brillout commented 3 years ago

Currently, when using Client-Side Routing (see docs here and there), the client-side pageContext is fetched from the server.

Some people want to be able to navigate between pages without hitting the Node.js SSR server. I'm opening a ticket for dicussing this.

I'm willing to implement this but this will require a couple of days work, so don't expect this too soon.

👍 this ticket if you want this. The more 👍 this ticket collects the more will I increase the importance of this. And better yet, add a comment elaborating why this is crucial for you and I'll then also increase urgency.

Edit: Most of the time, this is only about (negligible) server cost servings. For most apps, performance and cost implications are negligible. (Note that the static middleware of your Node.js server always needs to be hit anyways in order to get the latest code of the new page. It's mostly only about reducing Node.js work load.)

brillout commented 3 years ago

@Dema

I have no "loading" indicators while it is being loaded

Check out onTransitionStart and onTransitionEnd decribed at https://github.com/brillout/vite-plugin-ssr#import--useclientrouter--from-vite-plugin-ssrclientrouter.

I'd prefer this behavior only on the first page load, after page is loaded, it's better to use client-only routing without hitting the server at all. This will allow much smoother UX

Is your GraphQL server physically closer to your user than to your SSR server?

and it greatly reduces load of SSR server.

Yes you are right, with the current vite-plugin-ssr design, if your database doesn't live on the SSR server then the load for the SSR server is non-negligibly higher.

Dema commented 3 years ago

Yeah.. Is it closer, but the issue is that it will "freeze" the application while transitioning pages. I think, in the era of SPA's users are accustomed to instantaneous page transitions. I more like the idea behind Suspense, when I can delay loading state but still show it if it takes too long. I really like how next.js handles routing — after first page load it starts to use pure client-side routing.

It actually is less about load of the SSR server and more about user experience. For example, let's take CRUD, user is on the list page and clicks on one of the items to drop into details, as the list is already loaded into the graphql client's cache, actual useQuery would not go to grab data from back-end and can just instantaneously show details page, maybe while showing some spinners for parts that are not in the cache and still loading. But user can already interact with the page. This way SSR is perceived slower than the pure client queries.

I think it would be really great if you can expose full routes in AST form so we can create compilers for each specific routing library.

brillout commented 3 years ago

I do share the sentiment of being able to let vite-plugin-ssr do only the server-side HTML rendering and then break free from vite-plugin-ssr for subsequent client-side navigation.

The question here though is how would the data fetching work for e.g. todo-list.page.js?

Dema commented 3 years ago

There are two ways to do fetching in SSR 1) manually get everything loaded in server.js and hydrate-dehydrate 2) Do multi-pass rendering, first time gather queries, and the second time render actuall app.

I always do second one for my sites, because it is much more convenient for large apps. I'm using urql with react-ssr-prepass. This way I don't do fetching in .server.js, I do it as usual in useQuery on my pages and nested components. And in addPageContext I'm just gathering all those queries and then wait for them all to resolve

export async function addPageContext({
  Page,
  pageProps,
  graphqlClient,
  ssr,
}: PageContext) {
  const app = (
    <Suspense fallback={null}>
      <PageLayout graphqlClient={graphqlClient}>
        <Page {...pageProps} />
      </PageLayout>
    </Suspense>
  );
  await ssrPrepass(app);
  const urqlState = JSON.stringify(ssr.extractData());
  return { urqlState };

react-ssr-prepass uses Suspense to gather all Promises and wait for them. The same goes for apollo (they have getDataFromTree) rtk-query and react-query took first approach and require to to manually prefetch all queries

brillout commented 3 years ago

So you basically centrally manage the data fetching for all pages in your _default.page.server.js and in your _default.page.client.js. Consequently, entirely breaking free from vite-plugin-ssr is a viable option for you.

Now imagine a vite-plugin-ssr user who uses GraphQL, e.g. for todo-list.page.js, and also RPC, e.g. for account/create.page.js. Breaking entirely free from vite-plugin-ssr is not a viable option anymore. Otherwise, how would the user specify what happens when the user client-side navigates between todo-list.page.js and account/create.page.js?

That's the main blocker of this ticket. In a nutshell: it's not clear how to define data fecthing on page-by-page basis.

Dema commented 3 years ago

As I understand, it is already done by making all queries in addPageContext. We can just call it on the client without hitting SSR server after initial page load.

brillout commented 3 years ago

Exactly, that's why the plan here is to have addPageContext() to be called on the client, which today is not possible (addPageContext() hooks are defined in .page.server.js).

See my last edits to https://github.com/brillout/vite-plugin-ssr/issues/95#issue-919745792 for an update about this.

brillout commented 3 years ago

Also, just so we are on the same page here, this is only about server cost savings (and for a very small number of app architectures a performance benefit).

There is nothing you could achieve with client-side only navigations you cannot already achieve with the current design.

Dema commented 3 years ago

As I said this is not really about performance or reducing SSR server loads, it's more about perceived application performance when page transitions are generally expected to be instantaneous.

I like how next.js solves this with their <Link> component. They simply reuse routing information on client only without hitting server on page transitions that happen via <Link> tags. And still allow to do server-side transitions with plain tags if needed.

I think if you just allow us to get full routing information on application start then we can build our routes from .route.js files and forget about SSR stuff altogether. This way if someone needs, they can use existing .server.js pages to do per-page prefetching on every page transition, and also can do client-only transitions if they wanted so.

As I understand we can already use client-only routes in current vite-plugin-ssr, it's just a bit inconvenient because we have to have two copies of routing data in .route.js and in something like react-router-dom and keep them in sync.

brillout commented 3 years ago

it's more about perceived application performance when page transitions are generally expected to be instantaneous.

See https://github.com/brillout/vite-plugin-ssr/issues/95#issuecomment-860174345.

Check out examples/react/ or examples/vue/ and see how onTransitionStart and onTransitionEnd are used to implement animated smooth page transitions.

gryphonmyers commented 3 years ago

As I said this is not really about performance or reducing SSR server loads, it's more about perceived application performance when page transitions are generally expected to be instantaneous.

I like how next.js solves this with their <Link> component. They simply reuse routing information on client only without hitting server on page transitions that happen via <Link> tags. And still allow to do server-side transitions with plain tags if needed.

I think if you just allow us to get full routing information on application start then we can build our routes from .route.js files and forget about SSR stuff altogether. This way if someone needs, they can use existing .server.js pages to do per-page prefetching on every page transition, and also can do client-only transitions if they wanted so.

As I understand we can already use client-only routes in current vite-plugin-ssr, it's just a bit inconvenient because we have to have two copies of routing data in .route.js and in something like react-router-dom and keep them in sync.

I have a PR in progress that will allow for integrations with proprietary routers. This will avoid the duplicate route information problem and allow you to take over client routing. My focus is Vue Router but the underlying work should make a React Router integration viable.

Axxxx0n commented 3 years ago

I think the best option is NOT to call pageContext API endpoint when .page.server.js is not available for page requested.

brillout commented 3 years ago

I think the best option is NOT to call pageContext API endpoint when .page.server.js is not available for page requested.

I agree. Is the current behavior causing you trouble? Or is it an optimization you'd like to have at some point?

brillout commented 3 years ago

Updated OP to clarify what this is about.

Axxxx0n commented 3 years ago

IMHO client routing should not be blocking, because then it is almost the same as server routing in terms of UX. There are also a lot of discussions about this in nextjs #https://github.com/vercel/next.js/discussions/23921 I agree that server cost savings should be negligible but impact to user experience is huge - especially on slow networks.

brillout commented 3 years ago

I can also see that no fetching request at all VS fetching an empty response to make a considerable difference on mobile.

Were you able to reproduce a difference in performance or are you guessing so far?

I can implement skipping when there is no addPageContest sooner rather than later, as I expect this to be considerably faster to implement than OP.

On Sat 11. Sep 2021 at 14:05, Arian Santrach @.***> wrote:

IMHO client routing should not be blocking, because then it is almost the same as server routing in terms of UX. There are also a lot of discussions about this in nextjs #vercel/next.js#23921 https://github.com/vercel/next.js/discussions/23921 I agree that server cost savings should be negligible but impact to user experience is huge - especially on slow networks.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/brillout/vite-plugin-ssr/issues/95#issuecomment-917395968, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAHVQRQVCNJXTI4BIY3WVTDUBNAW5ANCNFSM46TQ2VNA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

Axxxx0n commented 3 years ago

No fetch request at all is the only viable option in my opinion because on slow networks client routing will still be blocked for the amount of response time. For example, if fetch request response time is 500ms, client routing will be blocked for minimum of 500ms. Without this request we can fetch data on client and show loading skeleton/spinner right away.

I can implement skipping when there is no addPageContest sooner rather than later, as I expect this to be considerably faster to implement than OP.

Sounds great, fixes my problem because I can use react-ssr-prepass to fetch data and don't need per page fetches.

Dema commented 3 years ago

The whole idea of hitting SSR server on each page transition brings us back to pre-SPA web. When user have to wait until server sends them rendered page on each navigation. SSR meant to help with SEO primarily and with loading data for slow networks at first page load. There's absolutely no need to do any SSR after user loaded page initially. All other navigation should be client-only. Next.js recently did similar thing with its getServerSideProps, that just rendered it unusable. They do the same "call SSR on each page navigation" thing and it slows down things tremendously. For example I have a web site where we render wordpress pages via rest api and render time jumped from "instantaneously open page and show some spinners" to "1.5 sec pause before page is even opened on 3G networks" after I tried to convert from getInitialProps to getServerSideProps. getInitalProps run both on client and server. On client it just doesn't receive http req/res objects (IIRC).

Axxxx0n commented 3 years ago

or example I have a web site where we render wordpress pages via rest api and render time jumped from "instantaneously open page and show some spinners" to "1.5 sec pause before page is even opened on 3G networks"

Thats why I also still use getInitialProps in Nextjs because is the only way to do performant client routing. If this library would implement client-side routing without hitting the server it would be major advantage.

brillout commented 3 years ago

I'm finishing up the 0.3.0 release. I'll re-consider this ticket after the release.

brillout commented 2 years ago

addPageContext() has been renamed onBeforeRender() in v0.3.

Alright, so the plan is following:

This means that if you move all your onBeforeRender() hooks from .page.server.js to .page.js then client-side routing will never make a request to your Node.js server.

I'm not sure what should happen if there are two onBeforeRender() hooks defined: one in .page.js and another in .page.server.js. I'll probably throw a [Wrong Usage] exception until someone found a useful use case for it.

Thoughts?

Dema commented 2 years ago

Looks great!

Axxxx0n commented 2 years ago

Sounds awesome and very flexible. Looking forward to this :)

brillout commented 2 years ago

ETA this week.

brillout commented 2 years ago

Next 7 days*.

brillout commented 2 years ago

Delayed. New ETA next week.

brillout commented 2 years ago

I started the implemention.

brillout commented 2 years ago

vite-plugin-ssr doesn't make superfluous .pageContext.json requests anymore.

Now working on allowing onBeforeRoute() to be defined in .page.js files (instead of .page.server.js); if we define all our onBeforeRoute() hooks in .page.js files then the Client Router will not hit the server (except for fetching static files).

brillout commented 2 years ago

vite-plugin-ssr doesn't make superfluous .pageContext.json requests anymore.

That part is now released in 0.3.8.

Now working on being able to define onBeforeRender() in .page.js files.

Axxxx0n commented 2 years ago

This is great! We could also prefetch static files with Intersection Observer API or similar on <a> tags depending on link so that the client routing can be instantaneous.

brillout commented 2 years ago

This is great! We could also prefetch static files with Intersection Observer API or similar on <a> tags depending on link so that the client routing can be instantaneous.

Yes that'd be exciting. Is the Intersection API really needed? Isn't a passive mouse listener with getBoundingClientRect enough?

If someone's up on working on that, let me know.

Axxxx0n commented 2 years ago

As far as I know Intersection Observer API doesn't run on main thread as opposing to passive mouse listener with getBoundingClientRect.

Implementing intersection detection in the past involved event handlers and loops calling methods like Element.getBoundingClientRect() to build up the needed information for every element affected. Since all this code runs on the main thread, even one of these can cause performance problems. When a site is loaded with these tests, things can get downright ugly.

https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

I can make rough implementation so we continue discussion. ;)

brillout commented 2 years ago

I can make rough implementation so we continue discussion. ;)

Sounds like a plan :-).

brillout commented 2 years ago

I may finish this ticket today btw. Hopefully actually since I'm on a wedding this weekend so limited working capacity.

brillout commented 2 years ago

@Axxxx0n How about:

import { prefetchUrl } from 'vite-plugin-ssr/client/router'

function onLinkProximity(url: string) {
  await prefetchUrl(url)
  console.log(`The static assets for ${url} are now loaded.`)
}

So we can do whatever we want on top of this.

Axxxx0n commented 2 years ago

I was just writing the same. This way we can prefetch manually from router, intersection on links, on link hover or similar.

brillout commented 2 years ago

Exactly

Axxxx0n commented 2 years ago

General thing is that we need to get context and call loadPageFiles in prefetchUrl but i dont't think we want fetch .pageContext.json that is fetched in getPageContext https://github.com/brillout/vite-plugin-ssr/blob/b4543b97f46d30037eb75072c70d0619a6c6abde/vite-plugin-ssr/client/router/getPageContext.ts#L59

dirty example:

async function prefetchUrl(url: string) {
    if(isExternalLink(url)) return
    if(!isNotNewTabLink) return
    const globalContext = await getGlobalContext()
    const pageContext = {
      url,
      _noNavigationnChangeYet: navigationState.noNavigationChangeYet,
      ...globalContext
    }
    addComputedUrlProps(pageContext)
    const pageContextAddendum = await getPageContext(pageContext) // gets .pageContext.json files
    objectAssign(pageContext, pageContextAddendum)
    loadPageFiles(pageContext)
}

Some possible solutions:

brillout commented 2 years ago
  • don't generate context in prefetchUrl, somehow just get _pageId from url and only call loadPageFiles

This

brillout commented 2 years ago

somehow just get _pageId

_pageId is provided by route() (see getPageContext.ts)

brillout commented 2 years ago

Btw we can also offer prefetchUrl(url, {prefetchPageContext: true}) for not only prefetching the static assets but also doing all the pageContext fetching. But I'm not sure what a concrete use case would need that? Seems quite extreme and I can't find a use case that justifies it, so far.

Axxxx0n commented 2 years ago

When this lands

Now working on allowing onBeforeRoute() to be defined in .page.js files (instead of .page.server.js); if we define all our onBeforeRoute() hooks in .page.js files then the Client Router will not hit the server (except for fetching static files).

this is not necessary

for not only prefetching the static assets but also doing all the pageContext fetching.

Because then we: have client onBeforeRoute and load everything on client OR have server onBeforeRoute which we want to load on server.

Axxxx0n commented 2 years ago

I can make rough implementation so we continue discussion. ;)

Sounds like a plan :-).

165

brillout commented 2 years ago

onBeforeRender() can now be defined in .page.js files in the latest release 0.3.11.

All issues mentioned in this ticket should now be resolved.

I will write docs for it in the next days.

brillout commented 2 years ago

https://vite-plugin-ssr.com/onBeforeRender-isomorphic