Closed brillout closed 2 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.
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.
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
?
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
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.
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.
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.
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.
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.
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.
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.
I think the best option is NOT to call pageContext API endpoint when .page.server.js
is not available for page requested.
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?
Updated OP to clarify what this is about.
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.
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.
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.
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).
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.
I'm finishing up the 0.3.0
release. I'll re-consider this ticket after the release.
addPageContext()
has been renamedonBeforeRender()
inv0.3
.
Alright, so the plan is following:
onBeforeRender()
hooks in .page.js
files (instead of .page.server.js
). Hooks defined in .page.js
files are isomorphic: they can be called in Node.js as well as in the browser. When doing Client Routing, they are called in the browser.onBeforeRender()
defined in its _default.page.server.js
nor in the page's .page.server.js
then there is no request made to the server.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?
Looks great!
Sounds awesome and very flexible. Looking forward to this :)
ETA this week.
Next 7 days*.
Delayed. New ETA next week.
I started the implemention.
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).
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.
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.
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.
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. ;)
I can make rough implementation so we continue discussion. ;)
Sounds like a plan :-).
I may finish this ticket today btw. Hopefully actually since I'm on a wedding this weekend so limited working capacity.
@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.
I was just writing the same. This way we can prefetch manually from router, intersection on links, on link hover or similar.
Exactly
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:
getContext
which prevents fetching .pageContext.json files for routeprefetchUrl
, somehow just get _pageId
from url and only call loadPageFiles
- don't generate context in
prefetchUrl
, somehow just get_pageId
from url and only callloadPageFiles
This
somehow just get _pageId
_pageId is provided by route()
(see getPageContext.ts
)
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.
When this lands
Now working on allowing
onBeforeRoute()
to be defined in.page.js
files (instead of.page.server.js
); if we define all ouronBeforeRoute()
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.
I can make rough implementation so we continue discussion. ;)
Sounds like a plan :-).
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.
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.)