Next.js Discord

Discord Forum

PPR behavior

Answered
Aleutian Tern posted this in #help-forum
Open in Discord
Avatar
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
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
View full answer

44 Replies

Avatar
Aleutian TernOP
next build outputs a lambda
And 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
Avatar
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
Answer
Avatar
Aleutian TernOP
Correct, the reason my page was dynamic was the use of headers()
Avatar
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?

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.
Avatar
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)
Avatar
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 data
With ISR and no suspense, isn't it supposed to replace the static shell?
Avatar
Aleutian TernOP
I mean, what's the proper way of doing ISR with PPR?
Avatar
Adding { next: { revalidate: int }} to your fetch() calls is the way I've been doing it. (also not sure if this is right)
Avatar
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
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
Avatar
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)
Avatar
Aleutian TernOP
Yes I find that weird
Avatar
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?
Avatar
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)
Avatar
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?
Avatar
What does the next build output say for that route
Avatar
Aleutian TernOP
Static
Avatar
Aleutian TernOP
Here's the simple reproduction repo https://github.com/dayblox/test
Image
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
Avatar
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>
  );
}
Avatar
Aleutian TernOP
Avatar
I don't think you should be getting this anyway, but potential workaround for now
Image
Avatar
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
Avatar
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