Next.js Discord

Discord Forum

Slow RSC page render leads to bad UX

Answered
American Crocodile posted this in #help-forum
Open in Discord
American CrocodileOP
I have a server component page in my app, similar to a blog post retrieved by its ID from the server. When creating a new post, the intention is to redirect the user to the newly created post. However, as it is an RSC (React Server Components) page, there is a delay in rendering on the server, resulting in a suboptimal user experience. This is particularly noticeable when opening the page for the first time without any caching involved. Typically, the rendering process takes around 1-2 seconds, excluding the API call to the server (which, in production, takes approximately 200ms, ruling it out as the issue in this case).

I attempted to address this by implementing a loading UI, but it only seems to work for the fetch from the server itself and not for the RSC render. Additionally, I experimented with prefetching this page after post creation using router.prefetch(), but unfortunately, that approach did not yield the desired results.

Is there any recommended method or approach to implement a loading state or any other technique to enhance the user experience in this scenario?
Answered by Ray
I think you could use useTransition hook
const [isPending, startTransition] = useTransition()
const {formState} = useForm()
const router = useRouter()

async function onSubmit() {
  startTransition(() => {
    router.push()  
  })
}

const isloading = isPending || formState.isSubmitting
View full answer

21 Replies

Philippine Crocodile
Perhaps I am mistaken, but since you are creating the page, should it not be a given that the initial page load after creation is going to be slower because the page has not yet been cached because it did not exist before, also since it is new it is going to take time to be generated/built on the server.
@Philippine Crocodile Perhaps I am mistaken, but since you are creating the page, should it not be a given that the initial page load after creation is going to be slower because the page has not yet been cached because it did not exist before, also since it is new it is going to take time to be generated/built on the server.
American CrocodileOP
Yeah, it's likely that since it's a dynamic page (even with force-dynamic), it might take some time to prerender on every request. However, is there a way to await this action and render a loading UI or something similar? In my case, there's a form that creates a post, and after successful creation, it should redirect instantly. Instead, it closes and waits 1-2 seconds before opening the new page. I would like to keep the submit button in a loading state during this process. I tried using router.prefetch with await, but it's not working (I assume it used to work in the pages directory using next/router).
@Philippine Crocodile Perhaps I am mistaken, but since you are creating the page, should it not be a given that the initial page load after creation is going to be slower because the page has not yet been cached because it did not exist before, also since it is new it is going to take time to be generated/built on the server.
American CrocodileOP
This works in pages directory with next/router but in app router (next/navigation) its not working since .push and .prefetch no longer returns a promise...
I also tried nextjs-toploader to smooth ux but it works only with <Link /> in app router, not with router.push
Philippine Crocodile
Are you using form actions to create the post? If possible I would wrap the component that is fetching the newly created post in a suspense boundary and have your loading component as the fallback
<Suspense fallback={<Loading />}><Post id={id} /> </Suspense>
I think this could get your loading UI showing up a little better
@Philippine Crocodile Are you using form actions to create the post? If possible I would wrap the component that is fetching the newly created post in a suspense boundary and have your loading component as the fallback <Suspense fallback={<Loading />}><Post id={id} /> </Suspense>
American CrocodileOP
It could work, but I'm using fetch at the top of the page with await, and I'm also using the generateMetadata function. Therefore, suspense isn't really a great choice in this case, in my opinion, especially for SEO. I'm looking for a solution to keep my submit button in a loading state until the RSC page is ready to show, or at least display a loading UI and then open. Unfortunately, I haven't found a solution so far. Additionally, there might be a chance that this dynamic page only renders slowly after build and only on the first time, and then renders fast for every subsequent request, even with different parameters, but I'm not sure about this theory.
Philippine Crocodile
I think it will probably render slowly the first time, I would still consider breaking out the post into a server component of it's own and then peform the fetch there, alternatively have a layout in that route segment and wrap the children in a suspense boundary (no sure about this one). Based on what I read about 'Streaming', the meta tags will be streamed in a little later, probably even before your page has finished rendering on the screen so google's bots will still be able to read them for your SEO purposes.
Have you also considered using the redirect function from 'next/navigation' - after creating the post, you can also call revalidatePath just before redirect in order to revalidate the cache.
@Philippine Crocodile Have you also considered using the redirect function from 'next/navigation' - after creating the post, you can also call revalidatePath just before redirect in order to revalidate the cache.
American CrocodileOP
Neither revalidatePath() nor router.redirect() returns a promise, so there's no way to hold the submit button in a loading state until the page loads. I attempted wrapping the entire page content inside Suspense with fallback, including the RSC fetch, but the user experience remains poor.

Initially, it loads for 1-2 seconds, then the Suspense page (loading.tsx) displays for approximately 10ms, followed by the Suspense fallback, and only then does the content show. Removing the generateMetadata function doesn't seem to resolve this issue, and the 1-2 seconds loading delay persists.
Philippine Crocodile
You should not need a return value for revalidatePath, it should just revalidate the cache so you can call it directly. Also the redirect function I mean is not the method on the router but a separate function itself. So it would be
redirect(`/posts/${id}`)
That would then redirect you to your desired post.
Answer
@Ray I think you could use `useTransition` hook ts const [isPending, startTransition] = useTransition() const {formState} = useForm() const router = useRouter() async function onSubmit() { startTransition(() => { router.push() }) } const isloading = isPending || formState.isSubmitting
American CrocodileOP
I've considered using useTransition, but I'm unsure of its suitability since it should redirect to a completely new page rather than just changing the content of the existing page/component. I might need to dive deeper into understanding how useTransition works to see if it can address my specific needs.
@Ray I think you could use `useTransition` hook ts const [isPending, startTransition] = useTransition() const {formState} = useForm() const router = useRouter() async function onSubmit() { startTransition(() => { router.push() }) } const isloading = isPending || formState.isSubmitting
American CrocodileOP
You're absolutely right. The challenge I'm facing is related to the process of creating a post, which involves a client-side function making an API call to the server, receiving the post ID, and then redirecting the user to the newly created post. Currently, I manage the form modal window's open state and loading state using useState inside the client component.

The issue arises after a successful API call. At this point, the submit button becomes active, the modal is closed (with setOpen(false) in the code), and the user is left looking at the basic page without any indication of loading until the RSC component is ready and the router redirects to the desired page.

In this scenario, I need either an instant opening of the new RSC page with no content, followed by fetching the post data, or I need to persist the loading state of the form and modal until the RSC page is ready to open instantly. The challenge lies in the fact that both redirect and revalidatePath don't return a promise to await until the page loads.
@Ray I use it with `useRouter` hook like this
American CrocodileOP
It's surprisingly working exactly as I expected. Thank you so much for your help
@American Crocodile It's surprisingly working exactly as I expected. Thank you so much for your help
this is how it works, useRouter is updating the state under the hood