vercel / next.js

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

Static Pages with dynamicParams Not Streaming & PPR Prefetch Not Caching Static Pages Properly #70911

Open hlege opened 1 month ago

hlege commented 1 month ago

Link to the code that reproduces this issue

https://codesandbox.io/p/devbox/static-page-render-zl2634

To Reproduce

1) Create a [slug]/page.jsx route. (In repo already done it) 2) Make a link for it. (In repo already done it) 3) npm run build -> npm run start

[slug]/page.jsx

import  { Suspense } from 'react';

export const dynamic = 'force-static';
export const dynamicParams = true;
export const revalidate = 20;

export default async function SuspenseTest(props) {
    const slug = (await props.params).slug;
  return (
    <div>
      <h1>This is a static content</h1>
      <Suspense fallback="Loading...">
        <LongRunning slug={slug}/>
      </Suspense>
    </div>
  );
}

async function LongRunning(props) {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  return <span>Success! ({props.slug})</span>
}

in other a route e.g app/page.jsx

import Link from "next/link";
import styles from "./page.module.css";

export default function Home() {
  return (
    <div className={styles.page}>
      <main className={styles.main}>
        <Link href="/suspenseTest" prefetch={false}>
          SuspenseTest
        </Link>
      </main>
    </div>
  );
}

Current vs. Expected behavior

If experimental.ppr is set to false:

Expected: When clicking the link, the static part of the page should load instantly, and the Suspensed part should load via streaming. Than cache it with the standard mode (ISR, SWR)

Actual: When clicking the link, the page doesn't load until the entire SSG rendering is complete, causing a 5-second delay before anything happens.

If experimental.ppr is set to true:

Expected: When clicking the link, the static part of the page should load instantly, followed by the Suspensed part via streaming. The rendered result should then be cached so that reloading or navigating to the page again loads it instantly without any loading indicator, whenever i open this page (via reload, paste the url, or client side navigation). After the revalidation is needed the standard ISR flow happens (Stale-While-Revalidate).

Actual: When clicking the link, the page loads with streaming, but the rendered result is only cached on the client side. On reload, the page is re-rendered via SSG (resulting in a 5-second delay), and it only loads once rendering is complete. After the SSG finished, reloading the page should load it instantly. However, navigating to the page again without a client cache should load it with streaming once more and not reusing the ISR cache.

Provide environment information

Operating System:
  Platform: PopOs Linux
Binaries:
  Node: 20.17.0
  npm: 10.8.2
Relevant Packages:
  next: 15.0.0-canary.179   
  react: ^18
  react-dom: ^18

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

Navigation, Partial Prerendering (PPR)

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

next start (local)

Additional context

This also occurs when Link has prefetch={true}, resulting in only client-side caching.

With the environment variable NEXT_PRIVATE_DEBUG_CACHE=1 enabled

The cache is only utilized when visiting the page directly. During client-side navigation, the following log is not generated:

using filesystem cache handler
get /suspenseTest undefined APP_PAGE false
set /suspenseTest
feedthejim commented 1 month ago

does this occur when no prefetch option is specified?

hlege commented 1 month ago

The prefetch option doesn't matter; it's the same either way.

wyattjoh commented 1 month ago

Previous to PPR, when a dynamic route did not specify a generateStaticParams, it was considered fully dynamic. The force-static here doesn't apply properly. With PPR enabled, it creates the page fully statically and therefore does not stream (because it can be fully statically rendered).

wyattjoh commented 1 month ago

You may be interested in enabling Partial Fallback Prerendering via the experimental.pprFallbacks = true option in your next.config.js file. That will allow more of the static shell to get served and streamed for dynamic pages.

hlege commented 1 month ago

Previous to PPR, when a dynamic route did not specify a generateStaticParams, it was considered fully dynamic. The force-static here doesn't apply properly.

Before streaming render it make sense to block request but now its not. (Also in the doc is says: When dynamicParams = true, the segment uses [Streaming Server Rendering](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense).

With PPR enabled, it creates the page fully statically and therefore does not stream (because it can be fully statically rendered).

In the example i provided if ppr is enabled it will stream render the static page but not caching it as SSG and only in the page reload or visit the page directly result as SSG render, but on client side navigation still fallback to dynamic streaming render even if it is generated as SSG page.

You may be interested in enabling Partial Fallback Prerendering via the experimental.pprFallbacks = true option in your next.config.js file. That will allow more of the static shell to get served and streamed for dynamic pages.

I will try it but i down know what it is doing.

I think in the build result the generated output log should also state it what will be streaming rendered cause its not so easy to debug it, and all the options should result with different results.

Update: If i understand right if i set experimental.pprFallbacks = true than it will fallback to stream render if static page with dynamicParams is rendering am i right? If i reload the same page it will show instantly without loading because it is cached via ISR.

If i navigate trough client side link or router for the first time (before it is generated with direct page load and cached via ISR) it will dynamic rendering with loader, but not populate the ISR cache. After reload still generating the page again.