vercel / next.js

The React Framework
https://nextjs.org
MIT License
124.67k stars 26.61k forks source link

Async generateMetadata hangs the app without any visible sign of loading #55524

Open wiwo-dev opened 11 months ago

wiwo-dev commented 11 months ago

Link to the code that reproduces this issue or a replay of the bug

https://github.com/wiwo-dev/generatemetadata-loading-issue https://codesandbox.io/p/github/wiwo-dev/generatemetadata-loading-issue/main

To Reproduce

  1. Run the application using the command: yarn run dev.

  2. Navigate to the root path (/). Here, you'll find two lists of "users" retrieved from jsonplaceholder. The list at the top has the problem as it uses generateMetadata. The one at the bottom doesn't use generateMetadata.

  3. The top list contains links to /user/[userId], and these links have a generateMetadata function that fetches data. Upon clicking these links, for the initial 3 seconds, there is no visible loading indication, making users believe that something might be wrong with the link.

  4. In contrast, if you click on any of the links in the bottom list, which leads to /user-no-metadata/[userId] that doesn't use generateMetadata, you'll notice that a Loading state is triggered by Suspense, providing a clear loading indication.

Current vs. Expected behavior

Current behaviour When a user navigates to a page containing a generateMetadata function that fetches data from a slowly working API, there's a problem. The application appears to hang, providing no indication that it's actively loading. Notably, the fetch operation within generateMetadata doesn't trigger React's Suspense, and the browser also fails to display any loading indicators.

To simulate a slow API response, a setTimeout is intentionally used within the getUsers function.

I encountered this issue while working on a project with a tens of thousands number of subpages, and it's highly likely that a given subpage will be opened for the first time and only once, with new entries continually being added.

Expected behaviour It should trigger the Suspense or show any kind of loading indicator in the browser so the user knows that something is happening under the hood and wait for the page to be displayed.

Verify canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: arm64
      Version: Darwin Kernel Version 22.4.0: Mon Mar  6 21:00:41 PST 2023; root:xnu-8796.101.5~3/RELEASE_ARM64_T8103
    Binaries:
      Node: 18.7.0
      npm: 8.15.0
      Yarn: 1.22.15
      pnpm: 6.11.0
    Relevant Packages:
      next: 13.4.20-canary.36
      eslint-config-next: N/A
      react: 18.2.0
      react-dom: 18.2.0
      typescript: 5.1.3
    Next.js Config:
      output: N/A

Which area(s) are affected? (Select all that apply)

Metadata (metadata, generateMetadata, next/head)

Additional context

Is there a way to display a loading state or any form of indication that something is loading while still utilising the generateMetadata function to fetch metadata information from a slow API?

jetaggart commented 11 months ago

We're also seeing this issue, which is a pretty big issue with for us. We have slow endpoints that generate our metadata and right now it feels like a tradeoff between bad SEO and using this feature. Is there a workaround to get metadata on the page without blocking or getting a loading ui/suspense boundary for page navigation?

I don't know if it helps but it seems the rsc is called twice in a row. If we have generateMetadata on, the first request takes as long as the metadata does to return, which doesn't seem to be caught by any suspense/loading ui boundary. If we take out the generateMetadata, both requests are still fired, but the first one is nearly instant with the second one taking some time but that is correctly picked up by the suspense loading boundary.

leon-marzahn commented 11 months ago

Same issue, couldn't generateMetadata be called compile time as well with a revalidation?

borispoehland commented 7 months ago

What helps a little is using unstable_cache to cache all subsequent requests via ISR:

import { unstable_cache as cache } from 'next/cache'

const getData = cache(
  (id) => fetch(...),
  ['getData'],
  { revalidate: 300 }
)

export async function generateMetadata() {
  const data = await getData();
  return { title: data.title };
}

Now it will block with getData on the first request (still bad), from then on always return cached data and revalidate in the background.

To not let it be blocking on the first request, you'd need a way of pre-running the getData on build time. Currently everything inside generateMetadata does not get called on build time

eijil commented 7 months ago

same !!!

Edit by maintainer bot: Comment was automatically minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote 👍 on the issue description or subscribe to the issue for updates. Thanks!

eijil commented 7 months ago

This should be a very commonly used feature, and it is surprising that the official team has not provided any fixes.

lpwprojects commented 6 months ago

Looks like this issue is the same as #45418

ArianHamdi commented 4 months ago

Same issue here. At least I expect generateMetadata to trigger suspense, and also I would like to bypass calling generateMetadata on client-side navigation, show the page instantly, and update metadata in the background.

jaakkohurtta commented 4 months ago

Sad to see this has not gotten any traction from the Next team as the issue is months old already. This is very lousy UX and is making me seriously consider moving my app back to pages.

Matt-roki commented 4 months ago

This is now a blocker for a production release on my app. Below are two screenshots of loading times, the first is excluding generateMetadata from the page file. The second is with generateMetadata resulting in a 4 SECOND SLOWER page load.

Screenshot 2024-05-06 at 16 27 51 Screenshot 2024-05-06 at 16 29 23

I have already cached the request with this function const getCachedData = cache(async (params) => { const { locale, slug, searchParams } = params; const preview = false; const domainVersion = process.env.DOMAIN_VERSION; return await fetchCommonData({ locale, preview, slug, domainVersion, searchParams }); });

It is then reused in the same page file for getting the pageData and the metadata.

Going to have to figure out a way to load the metadata after the first request is done...

steve-marmalade commented 4 months ago

Same issue here. At least I expect generateMetadata to trigger suspense, and also I would like to bypass calling generateMetadata on client-side navigation, show the page instantly, and update metadata in the background.

I do hope this issue gets attention, as @ArianHamdi's comment is exactly what I hoped/expected the functionality to be.

@Matt-roki , in the meantime, I was able to implement a work-around inspired by https://github.com/vercel/next.js/issues/45418#issuecomment-1914076788 that has seemed to resolve our issues.

popovidis commented 4 months ago

Same issue. Metadata taking ~6 seconds.

maxmdev commented 3 months ago

Any updates?

Edit by maintainer bot: Comment was automatically minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote 👍 on the issue description or subscribe to the issue for updates. Thanks!

RuslanAsadov commented 2 months ago

+, same problem

roman-veryovkin commented 1 month ago

Upvote the issue as well. Waiting for the solution out of the box, please)

Edit by maintainer bot: Comment was automatically minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote 👍 on the issue description or subscribe to the issue for updates. Thanks!

Matt-roki commented 1 month ago

Okay the best solution I have found for this is to remove the async part of generateMetadata.

Now for me all of my seo comes from a seo and that wouldnt work without async.

I now have a prebuild file which runs and generates a seo.json file that I can read my metadata from

"scripts": { ... "build": "node ./utils/generateSEO.js && next build", //this here runs a script to get all my seo data and sort it accordingly. ... },

Simplified code below.

export function generateMetadata({ params: { fullSlug } }) { try{ ... const seoData = jsonSEO.find(v => v.locale === locale && v.slug === findSlug) || {};

This fixes most issues the only downside is when a seo change is made a build is needed.

feedthejim commented 1 month ago

Hey, I wanted to give a few updates on this.

steve-marmalade commented 1 month ago

Thank you for the update @feedthejim .

While those enhancements seem worthy, they also don't seem like quick wins. Did you consider a solution similar to what @ArianHamdi wrote above (at least in the short-term):

I would like to bypass calling generateMetadata on client-side navigation, show the page instantly, and update metadata in the background.

I think that would go a long way to alleviating this issue, as we would be paying the cost on first-load only, whereas now it's per-page.

dannytlake commented 1 week ago

+1 for bypassing the blocker waiting for async generateMetadata to resolve during client side navigation.