vercel / next.js

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

Abilitty to skipping static pages generating at build time #45383

Closed hlege closed 1 year ago

hlege commented 1 year ago

Describe the feature you'd like to request

In next 13 with app directory there is no way to opt out from build time static page generation without loosing ISR. (Only dynamic render). If we want to use the same code base with different backend server (e.g configured in .env files) we force to use SSR and loose ISR.

These feature allows use case like dynamic portals with different backends servers and different data but the same pages and business logic.

Other issues is that the backend server often cannot be reach by developer/build machine.

In a big portal site this feature will also allow faster build and deploy time too, but preserve the ability to use ISR if needed.

Describe the solution you'd like

Configuration based settings:

const nextConfig = {
   ssg: false // means build time the pages wont generate static html, only runtime with ISR
};

Describe alternatives you've considered

Per paged configuration:

export const ssg = false;

or

export const preventSSG = true;
Xevion commented 1 year ago

Why don't you simply modify generateStaticParams to not generate any of the parameters at build time? Is there are particular use-case that makes a site-wide SSG disabling configuration necessary or abundantly better?

hlege commented 1 year ago

generateStaticParams only work pages with dynamic parameters (slug), if you has a page e.g "/about" with dynamic data coming from backend, then you cannot prevenet SSG, only export const revalidate = 0 works but then its become SSR only.

dbk91 commented 1 year ago

If your app directory component is marked as a client component, using the useSearchParams hook will omit the component from the static builds and force it to fetch client-side only.

During static rendering, any component using the useSearchParams hook will skip the prerendering step and instead be rendered on the client at runtime.

But I think I am confused about your use case. Can you clarify your first statement?

In next 13 with app directory there is no way to opt out from build time static page generation without loosing ISR. (Only dynamic render). If we want to use the same code base with different backend server (e.g configured in .env files) we force to use SSR and loose ISR.

Opting out of static page generation causes you to lose ISR because there is nothing to statically generate, hence Incremental Static Regeneration. The only option when opting out of a build runtime is to fetch it dynamically server side (e.g. getServerSideProps) because you'd be choosing to not run anything at build time. I'm not sure how you would be able to skip statically generating a page while simultaneously using ISR without that being the definition of fetching data dynamically server-side.

Please let me know if I misunderstood your problem.

hlege commented 1 year ago

I will try to explain it via example.

Given a page named /about . This page uses data fetched from backend. The backend data will change rarely or never change at all.

My problem is that the backend server cannot be reached at build time. Also I want to use the same application with different backend server.

So right now in order to build the application I am forced to use dynamic render (SSR). The problem with SSR is that every request to the /about page will dynamically fetch the data (which "never" changes ) and render the html. Of course I can make a custom cache that store the fetched data, but still the html will render every request. Also there is no cache control headers because it's SSR, so every request will hit backend even if it is still the same result every time.

So if we generate a static html on demand (not in build time) the nextjs features like cache headers control and ISR still works without the need to create a custom server.

Sharpiro commented 1 year ago

If we're explicitly saying "use client" at the top of the file, can't this disable the server-side stuff. Annoying to have errors about "localStorage" not available because a client component is trying to render on the server initially for some reason.

Sharpiro commented 1 year ago

There should be a “use client only”

dbk91 commented 1 year ago

@hlege You simply want to avoid data fetching during the build process due to backend resources potentially being unavailable, but you want it to fetch and render statically with ISR once deployed, correct?

If this is the case, I think you could simply avoid fetching the data within your server component by referencing a build time constant.

async function getPosts() {
  const res = await fetch(`https://.../posts`, { next: { revalidate: 60 } });
  const data = await res.json();

  return data.posts;
}

export default async function PostList() {
  let posts = [];
  if (process.env.BUILD_ENVIRONMENT !== 'local') {
    posts = await getPosts();
  }

  return posts.map((post) => <div>{post.name}</div>);
};

In the example, process.env.BUILD_ENVIRONMENT would be a user-defined build constant used to differentiate between your local environment and the deployed environment. This is of course assuming you are using a single static route and not any dynamic segments. For dynamic segments, I would recommend the solution @Xevion mentioned as it is more elegant.

benderillo commented 1 year ago

@dbk91

@hlege You simply want to avoid data fetching during the build process due to backend resources potentially being unavailable, but you want it to fetch and render statically with ISR once deployed, correct?

If this is the case, I think you could simply avoid fetching the data within your server component by referencing a build time constant.

async function getPosts() {
  const res = await fetch(`https://.../posts`, { next: { revalidate: 60 } });
  const data = await res.json();

  return data.posts;
}

export default async function PostList() {
  let posts = [];
  if (process.env.BUILD_ENVIRONMENT !== 'local') {
    posts = await getPosts();
  }

  return posts.map((post) => <div>{post.name}</div>);
};

In the example, process.env.BUILD_ENVIRONMENT would be a user-defined build constant used to differentiate between your local environment and the deployed environment. This is of course assuming you are using a single static route and not any dynamic segments. For dynamic segments, I would recommend the solution @Xevion mentioned as it is more elegant.

Your solution is like a hack around a limitation of the framework. And what @Xevion suggested applies only to the dynamic segments and even then it does not work (maybe a bug in nextjs13): if you have a dynamic segment page say you have a folders like this: "/posts/[slug]" and inside there is a page.tsx just like in their documentation

type Props = {
  params: {
    slug: string;
  };
};

export default async function Page({ params: { slug } }: Props) {
  const post = await getPost(slug);
  return (
    <article>
      {post.title}
    </article>
  );
}

When I try to build this with yarn build, I get the build error because getPost is called with "[slug]" as the value of the slug parameter passed into Page(). That is because I do not provide the generateStaticParams function. Silly me assumed that this will just skip the SSG and I can have SSR. I can make it work by exporting the export const dynamic = 'force-dynamic'; then it will force "no-cache" and I get SSR. Like explained here: https://beta.nextjs.org/docs/api-reference/segment-config#dynamic

But what the topic starter and I actually want is the ability to have a cache control with "generate on first request" (if I understood the request correctly).

Imagine you have a page that is not generated at build time but set to SSR with a cache control that says "revalidate = false"; basically once rendered it stays cached ( as if it was SSG).

This is what ideally would be the most suitable solution for the author of this ticket. Imagine a blogs website with a couple thousands of posts. I don't want to SSG them. Ideally I want them to be SSR on the first request with cache TTL set to like forever. So the first time someone tries to open a post, it gets rendered and cached. Next time it is the cache all the way. And If I want to invalidate, I can have an API route that would do manual revalidation of any page like it is supported in Nextjs 12.

dbk91 commented 1 year ago

Your solution is like a hack around a limitation of the framework. And what @Xevion suggested applies only to the dynamic segments ...

Yes, I am aware. I called both of those points out specifically in my original comment. My intent was to provide some solution to an uncommon use-case.

Imagine a blogs website with a couple thousands of posts. I don't want to SSG them.

I don't think a blog is the best example for the author's original scenario. The author has a limited access to the resources required to generate the pages at build time. I don't think this issue was about avoiding the cost of generating content at build time. It's typically ideal to generate these at build time so that the first user to visit the page doesn't incur the cost of waiting for the page to generate using SSR. This is also what enables the effectiveness ISR — the worst-case performance scenario is the user views stale content as opposed to waiting for a page generated using SSR.

hlege commented 1 year ago

i am well aware that i can use build time env for that, but than everything need to be checked and its a little "ugly" and i thinks nextjs can support it out of box. Also i think a common cms/portal like webpage is use a render strategy like: render when user request a page -> then cache it until its expired or content is changed.

the worst-case performance scenario is the user views stale content as opposed to waiting for a page generated using SSR.

In nextjs 13 the streaming rendering already solve this problem with a proper page layout structure too.

dbk91 commented 1 year ago

@hlege Totally agreed. I realize in retrospect that my eagerness to help you find a near-term solution, albeit a hack, was seemingly dismissive of your original feature request. My apologies for that. To be clear, I do agree something like this would be useful for the framework to expose — in particular the per page configuration at a minimum.

Tangentially, I found an issue specific to client components when trying to leverage the loading and error special file structure with SWR/suspense that a solution like this could fix more elegantly. SWR requires fallbackData when using suspense but that causes the application to flash content with the content of fallback data. This doesn't work well since client-side data might be specific to a user or frequently updated and will flash intermittently between the loading state and the pre-generated content. But if you don't provide content, it throws an error at build-time. The only workaround I found was to use useSearchParams to opt out at build time and then omit the fallbackData, but you have to place it before the useSWR hook, which is not obvious. It feels inelegant if you're not referencing search parameters in your component.

Of course, that example is not applicable to your issue since that's specific to client components and client-side fetching. I simply wanted to provide that to present another use-case to support this feature request.

benderillo commented 1 year ago

@dbk91

I don't think a blog is the best example for the author's original scenario. No, it is not :) I added it as a generalisation of the issue. Because what the author asks will handle both.

the worst-case performance scenario is the user views stale content as opposed to waiting for a page generated using SSR. I don't think we should be worrying about SSR performance too much these days. Look at the Remix framework that is all SSR deliberately but I doubt it has performance drawbacks compared to NextJS (they actually show in their blog that they are faster). Rendering is moving to the Edge and that makes SSR blazingly fast :)

dbk91 commented 1 year ago

@dbk91

I don't think a blog is the best example for the author's original scenario. No, it is not :) I added it as a generalisation of the issue. Because what the author asks will handle both.

the worst-case performance scenario is the user views stale content as opposed to waiting for a page generated using SSR. I don't think we should be worrying about SSR performance too much these days. Look at the Remix framework that is all SSR deliberately but I doubt it has performance drawbacks compared to NextJS (they actually show in their blog that they are faster). Rendering is moving to the Edge and that makes SSR blazingly fast :)

There are some good points here. I think it's important to keep in mind that SSR can be negatively impacted if the API you're integrating with is slow. For example, I have some personal projects where I serve content from my Notion via their API. In my experience, it's been slower than most so I try to do everything at build time. 😅 Having said that, you're correct in that it's up to the developer to determine those scenarios and edge runtimes can help this tremendously. And as @hlege pointed out, the appDir does provide improved performance if you leverage the layout structures in a fashion where you can build the majority of the static content and then display instant loading states otherwise.

I think this feature could be an overall win for the framework.

agusterodin commented 1 year ago

We have a similar requirement at my organization.

Our application has whitelabeling requirements (eg: custom brand name in browser title bar). We need to deploy run multiple versions of the same exact application build multiple times but with slightly different environment variables. The application needs to be deployed via Docker to custom infrastructure (customer requirements, not ours). We want to avoid performing multiple docker builds and maintaining many redundant docker images just to change a few build-time environment variables. Our proposed solution is to use runtime environment variables.

We are able to accomplish runtime environment variables with publicRuntimeConfig. I want to use getStaticProps so we can take advantage of incremental static regeneration and not take the performance hit that SSR would cause. Unfortunately there is no means to perform something similar to fallback: 'blocking' for non-dynamic routes.

I don't even need to revalidate as we don't have any blocking data requirements. For all I care I would set revalidate to an arbitrarily high value so that revalidation never happens.

Ideally there would be a way to tell the server to generate and cache ALL pages when the built server process starts (similar to what happens with static optimization during build time).

Basically what i'm looking for is "run-time" static page optimization instead of "build-time" static page optimization. We have a very small amount of pages, so run-time static optimization slowness isn't a concern.

If this isn't currently possible, does the app directory / Suspense make this possible?