PPR behavior
Answered
Aleutian Tern posted this in #help-forum
Aleutian TernOP
I am using app router, with a layout > a page > a component > a Suspense boundary
The Suspense boundary is abstracted by components, and is not directly in the layout nor the page.
Shouldn't the page including the nested suspense be PPR rendered when building? Because it isn't :/
PPR enabled with latest canary
The Suspense boundary is abstracted by components, and is not directly in the layout nor the page.
Shouldn't the page including the nested suspense be PPR rendered when building? Because it isn't :/
PPR enabled with latest canary
Answered by Matt
Here's the behavior:
Component not behind suspense that do data fetching etc... will be pre-rendered and served part of the static shell (use ISR to revalidate)
Component not behind suspense that accesses headers, cookies, or searchParams: will opt the entire route and children into dynamic (bad, wrap with suspense)
Component behind suspense: will be streamed in on the same request after the static shell
Component not behind suspense that do data fetching etc... will be pre-rendered and served part of the static shell (use ISR to revalidate)
Component not behind suspense that accesses headers, cookies, or searchParams: will opt the entire route and children into dynamic (bad, wrap with suspense)
Component behind suspense: will be streamed in on the same request after the static shell
44 Replies
Aleutian TernOP
next build
outputs a lambdaAnd I do not get any static shell during the lambda cold start (2s)
I've tested through a Vercel deployment, but I'll try that out when I'm back in a couple of hours
There are some dynamic components that I chose not to put behind a suspense boundary in order to avoid flicker, since it loads quickly
So for PPR to work, every dynamic component needs to be behind a Suspense ?
If I remember correctly, in the PPR introduction video, Lee adds Suspense one at a time
So my case with no shell is still weird, I'll test this and come back with the results
This might be right
Also, the dynamic with no suspense is cached for 5 minutes
Here's the behavior:
Component not behind suspense that do data fetching etc... will be pre-rendered and served part of the static shell (use ISR to revalidate)
Component not behind suspense that accesses headers, cookies, or searchParams: will opt the entire route and children into dynamic (bad, wrap with suspense)
Component behind suspense: will be streamed in on the same request after the static shell
Component not behind suspense that do data fetching etc... will be pre-rendered and served part of the static shell (use ISR to revalidate)
Component not behind suspense that accesses headers, cookies, or searchParams: will opt the entire route and children into dynamic (bad, wrap with suspense)
Component behind suspense: will be streamed in on the same request after the static shell
Answer
Aleutian TernOP
Correct, the reason my page was dynamic was the use of
headers()
Aleutian TernOP
@Matt Related question, in your first scenario, with a component that isn't behind suspense, is pre-rendered and uses ISR to revalidate
Is it normal to get hydration errors when the content replaces the out of date shell?
Is it normal to get hydration errors when the content replaces the out of date shell?
There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
Hydration failed because the initial UI does not match what was rendered on the server.
Text content does not match server-rendered HTML.
That's strange... I'm learning as I go but I haven't hit that.
I guess what I don't understand is why the streamed in content would be replacing anything in the shell anyway, that should be static and non updating (although I was observing some similar behavior just without the hydration error)
Aleutian TernOP
I do have some
new Date()
related stuff, but I still struggle to see how it's any different than revalidating the fetched dataWith ISR and no suspense, isn't it supposed to replace the static shell?
Aleutian TernOP
I mean, what's the proper way of doing ISR with PPR?
Adding { next: { revalidate: int }} to your fetch() calls is the way I've been doing it. (also not sure if this is right)
Aleutian TernOP
Have you tried rendering time related stuff in these components?
It's like the old server/client hydration clash with timezones except it's static/streamed
It's like the old server/client hydration clash with timezones except it's static/streamed
At least I suspect it's the case, I'll test this further
And in my case it's not a timezone issue, it's a rendering time issue
I.e.: the shell has "5 min. ago" and "4 min. ago" gets streamed in and errors
I.e.: the shell has "5 min. ago" and "4 min. ago" gets streamed in and errors
I see the issue you're hitting. Not sure I understand why though. The static shell itself shouldn't be changed during the hydration process (only calling revalidatePath or refreshing)
That's my understanding at least. This is all new.
(and to answer directly: I haven't tried time related stuff not in a suspense boundary)
Aleutian TernOP
Yes I find that weird
Aleutian TernOP
But what's the alternative behavior? Should the use of
new Date()
in an ISR page make it dynamic or should it only run when the page is revalidated?It should only run when it's revalidated I'd think
ISR should build the static shell and that shell itself should never be changed even at hydration until it's revalidated
Sometimes with this it's hard to tell how much is a knowledge thing for me vs this being a canary experimental feature (for example client side nav performance is broken in my builds, waiting for the slowest component before even starting the transition. I had to move off PPR)
Aleutian TernOP
I just started a new project to try things out from scratch, I'm at a loss
I just have a single ISR component, but it's statically compiled
Never revalidates
Shouldn't it be PPR?
What does the next build output say for that route
Aleutian TernOP
Static
Aleutian TernOP
Here's the simple reproduction repo https://github.com/dayblox/test
It's the case even without PPR, and on next@latest, there's something I don't get about revalidation
Oh it does revalidate, it's just written as static
So PPR is enabled if and only if some component uses dynamic stuff (headers, cookies, searchParams) and is behind Suspense
Aleutian TernOP
Here's a code sample to reproduce
import { Dynamic } from "@/components/Dynamic";
import { Suspense } from "react";
export default async function Page() {
const { unixtime } = await (
await fetch("http://worldtimeapi.org/api/timezone/Europe/Paris", {
next: { revalidate: 2 }, // Triggers hydration errors when revalidating
})
).json();
return (
<div>
{/* Enables PPR for the page */}
<Suspense>
{/* Uses dynamic functions like headers() */}
<Dynamic />
</Suspense>
{unixtime}
</div>
);
}
Aleutian TernOP
I've opened an issue
https://github.com/vercel/next.js/issues/58701
https://github.com/vercel/next.js/issues/58701
for people looking to debug this, you can mark the page as static so it triggers an error more explicitely when you have dynamic dependencyes
Aleutian TernOP
Even if I hide the errors, is it supposed to stream in the revalidated data? Before PPR, revalidation happened on the server for the next request, not this one