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

[NEXT-1187] Link navigation with loading.tsx is not instant for dynamic pages #43548

Closed
1 task done
tonypizzicato opened this issue Nov 29, 2022 · 107 comments
Closed
1 task done
Labels
area: app App directory (appDir: true) bug Issue was opened via the bug report template. linear: next Confirmed issue that is tracked by the Next.js team. locked

Comments

@tonypizzicato
Copy link

tonypizzicato commented Nov 29, 2022

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.1.0: Sun Oct  9 20:14:30 PDT 2022; root:xnu-8792.41.9~2/RELEASE_ARM64_T8103
Binaries:
  Node: 18.12.1
  npm: 8.19.2
  Yarn: 1.22.19
  pnpm: N/A
Relevant packages:
  next: 13.0.6-canary.2
  eslint-config-next: 13.0.5
  react: 18.2.0
  react-dom: 18.2.0

Which area of Next.js is affected? (leave empty if unsure)

App directory (appDir: true), Routing (next/router, next/navigation, next/link)

Link to reproduction - Issues with a link to complete (but minimal) reproduction code will be addressed faster

https://github.com/tonypizzicato/next-13-loading

To Reproduce

  • pull the repo
  • run yarn install && yarn dev
  • open localhost:3000 in the browser
  • go to the "Careers" page
  • reload the page
  • set some throttling in devtools
  • click different pages in the menu
  • notice that "Careers" page loading state and route change are done instantly but with some delay for other pages

Describe the Bug

When you have a dynamic page with loading.tsx the route change and showing the loading animation are instant only for the page, which was freshly loaded. For other dynamic pages it hits the server first, then shows the loading state
Screenshot 2022-11-29 at 23 19 38

next-13-loading.mov

Expected Behavior

Instantly navigate to the desired dynamic page and show loading component

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

NEXT-1187

@tonypizzicato tonypizzicato added the bug Issue was opened via the bug report template. label Nov 29, 2022
@Fredkiss3
Copy link
Contributor

That is how it works, in order to do the loading state, your browser has to first make the request to nextjs server, then nextjs will first return the loading state, and after resolves your page.

The thing is that in need, it is even slower that in production, because nextjs do a bigger amount of work. You could try to run it in production with yarn build && yarn start, but you'll find that it is not really instant, the slowness comes from the speed of your internet connection but also the distance between you and the server. If a normal request from you to the server is about 1s for example, then you'll see the loading state after 1s. But what is cool about the loading state if for example you make a request in your page.tsx which takes more that 5s (for example), you'll always see the loading state after 1s.

All of this is part of streaming rendering : https://beta.nextjs.org/docs/data-fetching/streaming-and-suspense#concepts

@tonypizzicato
Copy link
Author

tonypizzicato commented Nov 30, 2022

at is how it works, in order to do the loading state, your browser has to first make the request to nextjs server, then nextjs will first return the loading state, and after resolves your page.

that totally makes sense, that's why i've added throttling for testing on the client and delay on the api side. but isn't it a job of page prefetch feature to get the loading state before navigating to it?
and if that's an intended behaviour, why does the first page loaded work differently (instant loading state on client every time)?
and what is the reason to use streaming api with loading.ts feature in next.js if it is only useful (after 1s in your example) for API endpoints that take some significant time? for the most applications with simple db access endpoints its seems useless and client loading state would be preferred.

More than that Next.js docs say that loading.tsx should be instant compared to custom Suspense boundaries

Unlike loading.js, where navigation happens immediately and loading UI is displayed before the request to the server is complete. When manually defining Suspense Boundaries, navigation will happen after the new segment loads.
https://beta.nextjs.org/docs/routing/loading-ui#manually-defining-suspense-boundaries

I mean, I understand why it works the way you described for custom suspense boundaries, but it's not obvious from the docs that the loading.tsx should work the same way

@Fredkiss3
Copy link
Contributor

But what is cool about the loading state if for example you make a request in your page.tsx which takes more that 5s (for example), you'll always see the loading state after 1s.

As i said, for api endpoints that take some time to fetch (for example 5s) it would always take the same amount of time to show you the loading UI, the time it would take you to show the loading state is the time your browser to connect to the server. So if your browser takes 1s to connect to the server, the loading ui would show after 1s and not after 5s, so instead of waiting 5s to show your page, you get a loading state sooner. This is better for when you reload the page for ex.

And you're right, with prefetching it should be instant, since it has already the data... but it seems that when you have a loading state it prefetches twice ? 🤔
In the example below it does two requests to prefetch data (see by the next-router-prefetch header). I don't know if this is a bug or not :

Enregistrement.de.l.ecran.2022-11-30.a.12.13.09.mov

But prefetching isn't also a silver bullet. Take the case when your page hasn't finished prefetching the page, it would still take 1s before showing you the loading state and i think even if your page where static it would take the same amount of time : the time it takes for your browser to connect to your server and your server to serve you the static page.

Showing a loading state on the client would definitely make it faster, but only if you don't issue a navigation : like you fetch on the client without navigating to another page i think... 🤔

@balazsorban44 balazsorban44 added the area: app App directory (appDir: true) label Nov 30, 2022
@dumbravaandrei22
Copy link

Still happening in 13.1.3-canary.0

@dumbravaandrei22
Copy link

dumbravaandrei22 commented Feb 5, 2023

I think that the loading.tsx should be instant without any network calls.
I know that the loading.tsx will be under a page request, but like @tonypizzicato said the website should prefetch the Links that are on the page.
That's the idea, from a user's POV, the user should receive instant feedback from the app.
If the loading.tsx will take some time to load (even a small one) it will be useless.

@eug-vs
Copy link

eug-vs commented Apr 5, 2023

Absolutely agree with all problems raised in this issue, this is the only thing stopping our project from migrating to App Router - navigation creates just horrible experience with sometimes up to 300ms wait time before even the loading streams in (I know this can be solved by moving the server closer e.g. on edge, but that's a different story). Other side of the problem is that there's no way to display any other loading state on the UI while loading hasn't streamed in yet - you are forced into this UX.

I truly don't understand the reason behind this: if loading.tsx doesn't accept any props and is essentially static (usually just a bunch of skeletons), why can't we include it in client-side bundle (or at least opt-in to that)? Or event better - do not include it into a bundle, but fetch it and cache properly on the client - so that subsequent visits of the same page immediately show the fetch UI from client-side cache. That is probably how prefetch should work, but currently my experience is that it's working in a very random fashion (even in NextJS playground: https://app-dir.vercel.app/streaming/edge/product/2)

With Suspense it's a bit different because it allows for granular control, but even then the initial state where Suspense is showing fallback could be used with the same approach.

@hc0503
Copy link

hc0503 commented Apr 5, 2023

@tonypizzicato , I've resolved same issue with group route.
https://beta.nextjs.org/docs/routing/defining-routes#route-groups
Look at my app route structure.
image
Dynamic page uses the nearest parent loading.tsx, but announcements must has another loading.tsx.
To resolve this problem, I used group route.
I hope this solution works well for you.

@mhesham32
Copy link

for me when I added a root loading.tsx file it was picked immediately after I started the navigation to another page while the loading.tsx file inside the other page's folder wasn't picked and I was getting the same behavior as @tonypizzicato before adding the root loading.tsx file.
also the other SSR page is a search page and it gets the search query from URL search params I was expecting the loading.tsx to appear when I change the params within the page to fetch new data but instead, it was displaying the old results after clicking on search and then suddenly the new results replace them which is not good for UX it was like the site was unresponsive

@mhesham32
Copy link

it's worth mentioning the above behavior was happening with next@13.2.4 but when I upgraded to 13.3 it was no longer happening

@hc0503
Copy link

hc0503 commented Apr 11, 2023

@mhesham32 , did you use client component in 13.3?
that has error in my end.
#43930 (comment)

@mhesham32
Copy link

Yes the navigation is triggered from a client component to a server page

@sourcecodes2
Copy link

We are also experiencing this issue, and it is still present on v13.3.

The concept of "loading" for the end user means anything that is delaying their request, whether it's due to server processing or network latency.

There should be no distinction to the end user on technical merit. This is purely a UX issue.

End users need immediate feedback, whether that's the data they've requested or feedback to say their request is being handled.

Currently, this implementation fails to do this.

This is a core part of the age-old business arguement for retaining impatient users that we work ever so hard to get, that cannot be overlooked.

The fact that the on-request loading feedback needs loading on-request makes this implementation feel like one step forward and two steps back.

We are very grateful for NextJS and we do appreciate that the AppDir is still in beta, but please make sure this UX issue is fixed before release.

@Fredkiss3
Copy link
Contributor

It seems that to make loading instant you have to define it on the parent folder of the dynamic route like they do in App dir playground repository.

image

You can also use route groups like in @devdreamsolution comment : #43548 (comment)

@dumbravaandrei22
Copy link

It works with #43548 (comment) solution.
It doesn't work for non-slug routes like: 'feed/page.tsx'. I tried to create '(feed)/feed/page.tsx' having loading.tsx inside (feed) but it doesn't work.

I will postpone the use of app folder until will become stable.

@unleashit
Copy link

Glad to have found this issue for a partial fix. Hope the behavior changes for manual Suspense so that the fallback is shipped to the client and run at the start of the transition. That's the UX users are going to expect. I'm not sure what the behavior was pre app directory with getServersideProps. But in other isomorphic React projects I've worked on you could always show a loading state on the component level (not saying you couldn't in Next.js in a useEffect or getInitialProps, but my understanding is with GSSP it could only be done on the page level).

@Fredkiss3
Copy link
Contributor

Fredkiss3 commented Apr 21, 2023

@unleashit my understanding is that pre-app router the loading fallback could be controlled by the user with router events (https://nextjs.org/docs/api-reference/next/router#routerevents) there is even an example used for showing a loading indicator : https://github.com/vercel/next.js/blob/8050a6c8e056efb336f15f6fa5be789cd040574d/examples/with-loading/pages/_app.tsx

@unleashit
Copy link

Hey thanks for the reply but I'm pretty sure those router events were only for the full page (and not at the component level) as I was mentioning. I'm pretty sure Suspense fallbacks are intended as the solution, but I don't think it's implemented as well as it could be. It definitely an improvement over those router events, because you can control it on the "route segment" level with a child loading.tsx. That's better than it was, but better still would be to support the same behavior (fallback shown immediately on a client transition) in a manual Suspense fallback.

@Fredkiss3
Copy link
Contributor

@unleashit those events where for page changes (or URL changes I think ?), it is in a useEffect so you can use it even at component levels, it is basically the same as loading.tsx.

I would not say that Suspense is implemented badly,but what it seems is that the client router is not aware of the loading.tsx components, or more likely, it wait for things to suspend (with async/await or use) to show the loading fallback, you can try for example to export a simple non async component and put a loading.tsx beside the page, it would not show the loading fallback (at least that's my understanding, I may be wrong).

I also recently inspected the RSC payload on navigation, and have seem to find that a layout.tsx ships with a loading indicator, so it may be the fact that if a layout for a page is located in the same level as a loading.tsx file, when the layout will load, next navigations will show the loading fallback instantly as it has already prefetched it.

I will try this again and post an update with my findings later.

@unleashit
Copy link

unleashit commented Apr 22, 2023

Yeah I agree with most of that and was only using the router transition as a reference for how things were UX wise with the NextJS experience. Suspense is an upgrade over that because it has more granular support for loading content from the server, vs only the top/page level for getServerSideProps.

But I think there definitely are some issues that people will agree on and will hopefully get ironed out. As someone mentioned above, as a "user", I would expect either the content or a loading transition to appear immediately after clicking a link, button, etc. Not after a delay, whether the delay is caused by the network or a data request somewhere, because the distinction doesn't really matter to them.

The experience is different with a loading.js and a manual Suspense. In the docs, the former is basically an automatic suspense for a page, but the loading behavior isn't the same. As an example, you can go to: https://jasongallagher.org/portfolio/tatteasy and click next/prev buttons. You should see a loader (a nested loading.tsx) just between the header and footer (* at least in Firefox, see below).

But then the behavior isn't the same with manual . I have one on the home page part way down the page. You can test by clicking on "work" in navbar. If you refresh with throttling on in dev tools, notice that the checkerboard of images never shows a spinner. The site is statically exported, but I've also tried this in SSR mode (and these are server components that query data) and the behavior is the same: almost never a spinner, even with throttling. In some of my testing while running in SSR mode, I did sometimes see a blip of a fallback in some places, but not in the way I would expect. IMO for a good UX, the fallback state should last for the full time that the HTML markup can't be shown in the browser regardless of why.

I assume a static/SSG site doesn't show a suspense fallback because the RSC output is statically available. Yet the spinner works (albeit inconsistently) in Next's implementation of loading.js. It would be much nicer if the fallback was displayed consistently no matter the environment.

  • I just noticed while writing this up that the loading.js behavior works better in Firefox (which I was mostly testing on) than Chrome. In Chrome, the spinner sometimes appears and sometimes doesn't, even after clearing cache. Although now I'm wondering if there was a change after upgrading to 13.3.1 because it seemed to always work before.

@Fredkiss3
Copy link
Contributor

@dumbravaandrei22 maybe try to put a dummy Layout at (feed)/layout.tsx, maybe next skips your loading.tsx file because there is no layout at that level ? 🤔 i could be wrong though

@Fredkiss3
Copy link
Contributor

@unleashit Normally the Suspense boundary is supposed to always show if your page is SSR'ed on a full page refresh, that's because of streaming (something like this : https://locaci.fredkiss.dev/search). If it don't show with SSR, then it might be a bug, it might also be because your data fetching is very fast, what throttling does is only slow down you request to the server, if it is fast enough you will sometimes get a flash of loading states, or maybe not at all since when the browser receives the response your suspense boundary has already finished loading.

You could try to add an artificial delay to your fetches of for ex 1 to 2 seconds, to see it better.

@unleashit
Copy link

unleashit commented Apr 23, 2023

Normally the Suspense boundary is supposed to always show if your page is SSR'ed on a full page refresh

Yeah, that's exactly what I suspect the intention is but I don't think it's good UX. A better version of the Fallback would start running on client as soon as the dom is available, and/or as soon as a route transition begins if inside a layout. Then ending either as soon as content starts to paint, or when its finished (probably best). We can disagree on this, but there's no reason why it can't be implemented this way... and no reason I can think of that it wouldn't be a better user experience than a loader that appears before and/or after a pause. It already works like that in loading.tsx for the most part (seemingly with bugs), which is why you see the transition between pages on site that's static.

I get that it appears to work more nicely in situations where the server component has more work do (nice work on your site). But in other situations where the reason for the delay is network, browser parsing, etc, especially a page that already has content... it shouldn't matter what is causing the delay when it comes to fallback behavior. With client components we can use useEffect and have that control. But right now with server components you only have the behavior the Suspense fallbackoffers.

Interesting what you originally pointed out about the prefetched text files on my site. Just noticed it. I think they're just the static RSC output which for some reason on a static exported site have a txt extension (normally I think they're rsc). But I could be wrong!

@Fredkiss3
Copy link
Contributor

For link navigations manual <Suspense>, do not show until after a network request, instead the the current page is shown (because of useTransition), this is because react cannot know if one of your components will suspend or not, so it makes a request to the server and as soon as it finds a suspense boundary it automatically flush out a response with the fallback, then a little while after, it shows the complete page, you might have a request that only suspend on the first render (like the page i just shared, which suspends only on first request and uses client side fetching when navigating to make it fast for navigations).

For loading.tsx however it seems to work a little differently. Here is what i understand :

  • next try to get the loading.tsx for a route in the first navigation, then for the subsequent navigations it reuses the loading.tsx file if it is for the same segment.
  • If the loading is on a level just upper than the page (at the same level as a parent layout), next will use that loading.tsx directly.
  • For a different loading state that the one in the layout, you may add another layout in a route group with your loading.tsx file
  • One thing i noticed though, for loading to be instant, next needs to prefetch the page.

I have an exemple without prefetching which shows the loading spinner only after a delay then show them instantly on subsequent navigations

I also have an with prefetch=true that shows them instantly

it shouldn't matter what is causing the delay when it comes to fallback behavior.

I think the logic of the react team is that Suspense gives you the power to show the loading spinner whenever you like, by default it does not do exactly that, but you can take over this by using a transition, i've done something like that here : https://parallel-routes-modal-next.vercel.app/boards_search/oWNLDSH2nQTVrDcWS3zg7o

You can see here that the suspense boundary show up instantly because i suscribed to the isPending given by useTransition.

However this example is very hacky as i have to opt out of the link component entirely : https://github.com/Fredkiss3/parallel-routes-modal-next/blob/c9443997ebc8ccaadc03c0a68a2e8ab17ed1f260/src/app/boards_search/%5Bbid%5D/card-list.tsx#L41

I think the canonical solution here is the loading.tsx file, you just have to make sure to place them in the right place and add prefetching.

@unleashit
Copy link

unleashit commented Apr 23, 2023

I think it comes down to justifying behaviors because of what may seem to make sense "technically". But with user experience, you do your best to achieve behavior based on what the user would expect and prefer. If the cost is high, you sometimes have to compromise of course. I'm not going to have one of the first tickets to Mars for example. My UX is probably going to be going camping instead. But since I don't think this is quite rocket science and Sever Components are still new, it seems like a good time to focus on what the UX of it should be.

I think the acceptance of this is a Next thing... coming from the fact that getServersideProps was accepted as the norm even though it was always bad UX. There were always better ways to handle client route changes via lazy loading such as this, which allowed finer control of the experience that what GSSP offered. RSCs are an upgrade over both methods, but the fallback situation is still less than ideal. It may work fine in some situations, but in others it doesn't. There's no reason why React and Next can't fix it if they want. Just show the fallback as soon as the route transition begins and stop showing it when the rsc is ready to hydrate. I'm not an expert on Server Components, haven't watched any two hour videos and don't currently have time, but I just can't image what the technical barrier to that could be.

@Fredkiss3
Copy link
Contributor

You are totally right about the expectations of user experiences.

I don't know if it is "fixable" by the Next or React team, it doesn't seem like it for Suspense to me, but I may be totally wrong.

I'm totally in the same length as you about what we can do to make UX better.

My suggestions (with informations I have today) :

1 - use Loading.tsx for page transitions
2 - use Suspense for the first load if you want to have Streaming on just some components and takeover with useTransition to show loading fallback instantly on the client

In a live video with Ben Holmes (the whiteboard guy who works at Astro) & Dan Abramov, they made a little app with a Search list, they did the same as my second suggestion : https://github.com/bholmesdev/simple-rsc/tree/main/app

@unleashit
Copy link

I don't see why it isn't fixable. For each suspense, add a HOC to the client bundle that loads the fallback immediately on mount. Then end it once it has children. Seems like loader.js is already doing that (although not perfectly), and probably not far off from a normal suspense. I would think this is probably something for Next rather than React to do. Of course I'm being an arm chair directory who hasn't taken the time to look at the code, but if this issue is for some reason unsolvable than in my opinion is is a big mark against server components.

I appreciate the ideas! For my little project I won't sweat it too much. I've looked at useTransition and it seems more for not blocking the UI when doing some work? I'll have to check that out at some point.

@Fredkiss3
Copy link
Contributor

Fredkiss3 commented May 3, 2023

There is a PR about a potential solution for this : #49077

TLDR : next will now prefetch pages content until it finds a loading.tsx which will be prefetched also.

Edit :

Tested it on the latest canary and it works : https://github.com/Fredkiss3/next-13-loading-prefetched .
Before, next did not do any real prefetching (it did not run the page on the server), now next will prefetch the page until it finds a loading.tsx file, so that on navigation the loading fallback will show instantly, it works also with suspense.

For suspense there is something interesting : if the page is prefetched (either on hover on DEV, and by default on production), it will eagerly run and return the suspense boundary, and if suspense resolves before you navigates to that page, you won't see the fallback but the page directly. this one is pretty cool.

Updated this example which is more complete : https://github.com/Fredkiss3/parallel-routes-modal-next

@unleashit
Copy link

But it seems like the only logical fix is to have that in the client bundle in order for it to be instant.

Yes and

but honestly the “instant” loading feature feels misnamed, and I’m more or less convinced it shouldn’t be used at all if it’s not actually instant.

Both of these are the bottom line, and what I (and some others) tried to argue months ago.

export const include = true

This would be the opt-out method, which be a big improvement since it would fix more than this issue and give the dev a choice on how they want to balance performance (faster initial load vs faster transitions without relying on prefetch). Personally I prefer the (traditional) opt-in approach to code splitting, which is really as simple as a dynamic import per component (or page) you want to code split/lazy load.

@jvandenaardweg
Copy link
Contributor

Why would you want this to be configurable? Loading states should be instant, not after a fetch to whatever server. Its just bad UX.

@feedthejim
Copy link
Contributor

feedthejim commented Oct 10, 2023

I wanted to clarify a bit: the fetch happens pre-emptively when the page loads, not when you navigate.

Can anybody send me a repro where their navigation is slow? I would love to investigate. Any datapoints would help us evaluate or not if we should change that part of the architecture.

@unleashit
Copy link

unleashit commented Oct 10, 2023

@jvandenaardweg Configurable in some way (either as opt-in or opt-out of bundling) I think is important. If you automatically bundle every possible fallback/loading.js in an initial load, it could get bloated in some cases. But in my comment above, I was thinking about the bigger picture of having more control over bundling in general.

I wanted to clarify a bit: the fetch happens pre-emptively when the page loads

@feedthejim I think you're referring to prefetching. Prefetching is unreliable because it can either be turned off or the user can click a link before the response(s) resolve.

@jvandenaardweg
Copy link
Contributor

jvandenaardweg commented Oct 10, 2023

I wanted to clarify a bit: the fetch happens pre-emptively when the page loads, not when you navigate.

Can anybody send me a repro where their navigation is slow? I would love to investigate. Any datapoints would help us evaluate or not if we should change that part of the architecture.

It happens when you navigate, so clicking a link to a different page where that page is expected to show a loading indicator. So with a loading.tsx or a Suspense fallback. The above gif of @arackaf shows exactly what happens. He also has a reproduction repo.

To be more precise on where to look in that gif; its about the delay between the click and until the loading indicator is shown. Thát delay shouldn't be there.

@IGassmann
Copy link
Contributor

IGassmann commented Oct 10, 2023

@arackaf In your reproduction, it seems that you're running the app in development mode (next dev), and not in production mode (next build; next start). As per the docs, prefetching is disabled in development. If you run your app in production mode, the navigation is indeed instant after the prefetch has completed:

Arc.2023-10-10.at.09.50.15-converted.mp4

In production, routes are automatically prefetched as they become visible in the user's viewport. Prefetching happens when the page first loads or when it comes into view through scrolling.

@IGassmann
Copy link
Contributor

The fact that prefetching is not enabled in development is a common source of confusion on how this works. @feedthejim why is it disabled in development?

@unleashit
Copy link

So I guess people in the Next.js community feel that prefetching is a good solution. But a) it can be turned off in a Link and b) the user can act faster than the response comes in. This is in production. Lastly, it costs $ especially when serverless.

@feedthejim
Copy link
Contributor

@unleashit I'm asking for a real repro where I don't have to simulate 3G conditions to notice a delay

@IGassmann we disable prefetching in dev because it would force the compilation of all the pages that you're linking to from your page. Please do note that if you do click a link in dev, we simulate the prefetching behaviour however by fetching the loading state first and then the rest.

@IGassmann
Copy link
Contributor

@unleashit I'm asking for a real repro where I don't have to simulate 3G conditions to notice a delay

@feedthejim, this is a challenging request. Users may encounter poor internet connections, such as when using the app in the subway. For example, a user might load a page right before entering a tunnel and then, while in the tunnel, click a navigation link, which could result in no loading indicator. How do you provide a reproduction for this situation?

@unleashit
Copy link

@feedthejim sorry, I don't have time at the moment. I do have a non-minimal statically exported repo I could show you that never shows the loaders: https://github.com/unleashit/portfolio-v4/tree/master/app/portfolio. They were actually working at some point during the early app directory development, but the behavior changed several times and now nothing.

I think most people here are talking about SSR, where the loader can sometimes be delayed like in @arackaf s demo. Again, I used to get this behavior during the 13 beta for my static site, but now nothing. But I've also seen it in SSR sites.... just don't have any at hand to post. You'll need to throttle it though to see, unless you want to take your laptop maybe a block from the house/office until the connection is bad ;)

@jvandenaardweg
Copy link
Contributor

jvandenaardweg commented Oct 10, 2023

The issue also happens in production environments. The same RSC background fetch/prefetch call delays rendering of a loading indicator.

In production, routes are automatically prefetched as they become visible in the user's viewport. Prefetching happens when the page first loads or when it comes into view through scrolling.

I see the prefetching happen for the pages that are linked to. But the same fetch happens when a user clicks the link to that page.

  1. Hard refresh the current page you are in
  2. You'll notice the prefetching happening.
  3. On the current page, click a link to a different page, let's say it links to /page/whatever
  4. When clicked, a fetch happens in the background to /page/whatever?_rsc=17rda. It's the same fetch that already happened on page load.

Now, nothing happens on the screen until that fetch resolves. Not even a loading indicator. Nothing. And this is exactly the point we try to make by throttling your connection. Not everyone has sub 50ms connections to the server at all times to get a server respond if we should even show a loading indicator...

  1. When it resolved, the loading indicator of the page is shown
  2. After the RSC is done fetching some external/database data, the component is show

I tried making a demo video of the issue. But it's giving too much away of my environment, including url's that I don't want to be publicly available. But it's the same issue as posted in the GIF a few days ago in this thread.

So I guess people in the Next.js community feel that prefetching is a good solution. But a) it can be turned off in a Link and b) the user can act faster than the response comes in. This is in production. Lastly, it costs $ especially when serverless.

Also, this is worrysome yes. I already see a huge increase in calls that count up to my usage in Vercel, just by using the App Router.

@jvandenaardweg
Copy link
Contributor

jvandenaardweg commented Oct 10, 2023

I checked @arackaf 's repo and environment. This shows the same issue i'm also seeing:

Demo: https://next-instant-loading.vercel.app/
Repo: https://github.com/arackaf/next-instant-loading/tree/feature/instant-loading

@IGassmann
Copy link
Contributor

IGassmann commented Oct 10, 2023

@jvandenaardweg something is weird; I'm able to reproduce the issue on the app deployed on Vercel (next-instant-loading.vercel.app), but not when it's running locally in production mode (arackaf/next-instant-loading@feature/instant-loading)

On Vercel
https://github.com/vercel/next.js/assets/4291707/1fcc9092-352e-4db1-aaed-9f64e1713cff

Locally
https://github.com/vercel/next.js/assets/4291707/68c7f5bd-12ca-4bbb-a67a-d9cd1f2ed9ba

Edit: The app on Vercel is actually still pointing to an old commit (arackaf/next-instant-loading@f7de8a2), which still uses Next.js 13.5.2. That explains the discrepancy.

@jvandenaardweg
Copy link
Contributor

jvandenaardweg commented Oct 10, 2023

Edit: The app on Vercel is actually still pointing to an old commit (arackaf/next-instant-loading@f7de8a2), which still uses Next.js 13.5.2. That explains the discrepancy.

You sure @IGassmann ? The Preview environment on GitHub shows a succesful deploy of "Update Next" as the last succeeded deployment: https://github.com/arackaf/next-instant-loading/deployments/Preview , which points to 13.5.4: arackaf/next-instant-loading@2c90fca

"Update Next" is the last commit on the feature/instant-loading branch he was pointing to: https://github.com/arackaf/next-instant-loading/tree/feature/instant-loading

@IGassmann
Copy link
Contributor

@jvandenaardweg that successful deployment in the Preview environment is hosted at https://next-instant-loading-o7zx8tsok-adam-rackis.vercel.app/, not at https://next-instant-loading.vercel.app/

On that deployment, we can see the issue fixed:

Arc.2023-10-10.at.15.35.39.mp4

@arackaf
Copy link

arackaf commented Oct 10, 2023

Hey all - sorry, yeah the original prod repro may not be on the latest commit. I’ll take a look later and update.

I’m glad to hear it’s instant with a prefetch, but I would still feel much much much better if it was just included in the JS bundles for a number of reasons articulated above by others.

Instant loading state should not depend on a prefetch.

@jvandenaardweg
Copy link
Contributor

jvandenaardweg commented Oct 10, 2023

Thanks for clearing that up @IGassmann ! I'll have to take another look at my code then. Because i'm on the same latest Next version but dó see the delay. I'm just totally lost now what's causing it.

@jvandenaardweg
Copy link
Contributor

jvandenaardweg commented Oct 10, 2023

I guess my issue lies in the use of <Suspense> on a page to have fine grained control over what component is loading, instead of a page wide loading indicator.

Using Vercel's app router demo to demonstrate:

No issue here when using a generic loading.tsx file: https://app-router.vercel.app/loading

But the issue is here "Streaming with Suspense": https://app-router.vercel.app/streaming

When you throttle the connection, using loading.tsx instantly shows a indicator. Which is good. But using the same throttled connection on the Streaming With Suspense demo prevents anything from being shown until a certain point. Not really clear when that exactly is.

Both also tested with the latest NextJS version locally and with a deploy on Vercel.

So the issue lies in the use of <Suspense> with a fallback on a page.tsx. When I switch over to using the loading.tsx the issue dissapears in my project. But that's not what I hoped, I really want to use the Suspense fallback on a per component basis and not have a loading.tsx on the page level.

edit: issue about this: #54667

@arackaf
Copy link

arackaf commented Oct 10, 2023

Ok I've updated my original repro with the latest Next version, and the instant loading state does show instantly, given the prefetch.

I do still think loading into client bundles would be better, but it does look like this is essentially fixed and working as designed.

https://next-instant-loading.vercel.app/no-suspense

@unleashit
Copy link

It may be working as designed, but it doesn't work. Relying on a lazy loaded response can and will lead to weird glitches with loaders. Separately, I would expect loading.js (and IMO suspense fallbacks) even in static/SSG sites to show the fallback if no RSC for the page or component has yet been downloaded, which can happen when prefetch prop is set to false.

It's hard to not get the feeling that Vercel has planned for a heavy use of prefetching for their own concerns and we has devs should not just brush this under the rug and give them a pass. We are more or less forced by the industry to learn and use this tool, but it can be made better if you aren't afraid to give them a little pressure about it. Prefetching all the things is a terrible idea. Worrying about adding 10-20k to a bundle is good, but never without consideration about the overall balance.

@murilo1008
Copy link

I updated my project to the latest version of Next. In fact, loading has improved along with page change speed, but it is not yet in immediate development.

I used this command:

npm i next@latest react@latest react-dom@latest eslint-config-next@latest

Do I need to do anything else to make page switching instantaneous?

@arackaf
Copy link

arackaf commented Oct 10, 2023

@murilo1008 I'd create a simple repro and upload to Vercel. Feel free to fork mine above

@leerob
Copy link
Member

leerob commented Oct 12, 2023

I want to clarify how loading UI works in Next.js.

Routing in the App Router is "server centric". We need a network roundtrip to the server when doing route transitions. The client doesn't know what loading state to render, until it communicates with the server.

When we prefetch, we can retrieve both the route to navigate to and the loading UI. Prefetching is what makes navigations feel snappy. If you prefetch the loading UI which does not have data fetching, then you can "instantly" show this loading state, followed by the dynamic content on the page being streamed in after. This is enabled through React Suspense and streaming server-rendering as part of the App Router.

We don't believe an alternate solution where you include all of the routing variants on the client is feasible or the best solution for Next.js. For example, consider a loading state which is 100kb of HTML / possible routes. Should that be included in the initial JavaScript bundle, blocking interactivity for everything on the page until it's downloaded? Or, is it better to not block interactivity, and load it asynchronously before the next click? We still believe the current model is correct.

There's still opportunities for us to improve the experience of transitioning between loading states. For example, we can optimize further the de-duplication of data. Thanks for the reproductions here, as they'll help us dig in further and find places to optimize.

@Systemcluster
Copy link

Routing in the App Router is "server centric". We need a network roundtrip to the server when doing route transitions. The client doesn't know what loading state to render, until it communicates with the server.

Does this include loading states from already loaded layouts? I have a single loading.tsx with "use client" inside a layout.tsx with "use client" that is used across the whole app. Every route uses this loader. I would expect the already-loaded globally-used loader to be shown immediately without prior network roundtrip.

@unleashit
Copy link

unleashit commented Oct 12, 2023

Thanks for explaining the thinking. I also just came across Deep Dive: Caching and Revalidating which is also very helpful and appreciated, The docs (at least at the point I was reading them several months ago) don't go far enough into some of these subjects or explain the thinking behind them.

Personally, I wish Next.js kept to its original roots of Isomorphic code with getInitialProps. getServersideProps started this whole thing of letting Next mange client side cache which usually results in excess server requests. That I think defeats the main advantages of SPAs, which is offloading some of the expensive work to the client and the clean separation of backend concerns. I love most of the other things that got added (SSG, even the app directory and RSCs "in the right situations"), but getInitialProps was actively discouraged which IMO is mostly why the tide started turning back to the server. Not just with Next.Js, but that's when Remix, etc. started coming out. Anyhoo, it appears the same foundation is why we get loader glitches sometimes.

We don't believe an alternate solution where you include all of the routing variants on the client is feasible or the best solution for Next.js. For example, consider a loading state which is 100kb of HTML / possible routes.

I can imagine that being possible, which is why it would be nice amazing if you crafty people could figure out a way to make code splitting configurable

@github-actions
Copy link
Contributor

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 26, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: app App directory (appDir: true) bug Issue was opened via the bug report template. linear: next Confirmed issue that is tracked by the Next.js team. locked
Projects
None yet
Development

No branches or pull requests