-
Notifications
You must be signed in to change notification settings - Fork 27.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial implementation of PPR client navigations #59725
Merged
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Tests Passed |
Stats from current PRDefault Build (Increase detected
|
vercel/next.js canary | acdlite/next.js ppr-navigations-initial | Change | |
---|---|---|---|
buildDuration | 12.7s | 12.7s | N/A |
buildDurationCached | 7.1s | 6.1s | N/A |
nodeModulesSize | 200 MB | 200 MB | |
nextStartRea..uration (ms) | 426ms | 421ms | N/A |
Client Bundles (main, webpack) Overall increase ⚠️
vercel/next.js canary | acdlite/next.js ppr-navigations-initial | Change | |
---|---|---|---|
193.HASH.js gzip | 181 B | 182 B | N/A |
3f784ff6-HASH.js gzip | 53.3 kB | 53.3 kB | ✓ |
433-HASH.js gzip | 27 kB | 28.3 kB | |
framework-HASH.js gzip | 45.2 kB | 45.2 kB | ✓ |
main-app-HASH.js gzip | 240 B | 242 B | N/A |
main-HASH.js gzip | 31.7 kB | 31.7 kB | N/A |
webpack-HASH.js gzip | 1.7 kB | 1.7 kB | N/A |
Overall change | 125 kB | 127 kB |
Legacy Client Bundles (polyfills)
vercel/next.js canary | acdlite/next.js ppr-navigations-initial | Change | |
---|---|---|---|
polyfills-HASH.js gzip | 31 kB | 31 kB | ✓ |
Overall change | 31 kB | 31 kB | ✓ |
Client Pages
vercel/next.js canary | acdlite/next.js ppr-navigations-initial | Change | |
---|---|---|---|
_app-HASH.js gzip | 194 B | 195 B | N/A |
_error-HASH.js gzip | 183 B | 181 B | N/A |
amp-HASH.js gzip | 504 B | 502 B | N/A |
css-HASH.js gzip | 321 B | 321 B | ✓ |
dynamic-HASH.js gzip | 2.5 kB | 2.5 kB | N/A |
edge-ssr-HASH.js gzip | 255 B | 253 B | N/A |
head-HASH.js gzip | 350 B | 349 B | N/A |
hooks-HASH.js gzip | 369 B | 369 B | ✓ |
image-HASH.js gzip | 4.28 kB | 4.28 kB | N/A |
index-HASH.js gzip | 255 B | 256 B | N/A |
link-HASH.js gzip | 2.61 kB | 2.61 kB | ✓ |
routerDirect..HASH.js gzip | 312 B | 311 B | N/A |
script-HASH.js gzip | 385 B | 383 B | N/A |
withRouter-HASH.js gzip | 307 B | 308 B | N/A |
1afbb74e6ecf..834.css gzip | 106 B | 106 B | ✓ |
Overall change | 3.4 kB | 3.4 kB | ✓ |
Client Build Manifests
vercel/next.js canary | acdlite/next.js ppr-navigations-initial | Change | |
---|---|---|---|
_buildManifest.js gzip | 483 B | 484 B | N/A |
Overall change | 0 B | 0 B | ✓ |
Rendered Page Sizes
vercel/next.js canary | acdlite/next.js ppr-navigations-initial | Change | |
---|---|---|---|
index.html gzip | 528 B | 526 B | N/A |
link.html gzip | 541 B | 539 B | N/A |
withRouter.html gzip | 525 B | 522 B | N/A |
Overall change | 0 B | 0 B | ✓ |
Edge SSR bundle Size Overall increase ⚠️
vercel/next.js canary | acdlite/next.js ppr-navigations-initial | Change | |
---|---|---|---|
edge-ssr.js gzip | 93.7 kB | 93.7 kB | N/A |
page.js gzip | 146 kB | 146 kB | |
Overall change | 146 kB | 146 kB |
Middleware size
vercel/next.js canary | acdlite/next.js ppr-navigations-initial | Change | |
---|---|---|---|
middleware-b..fest.js gzip | 625 B | 626 B | N/A |
middleware-r..fest.js gzip | 151 B | 151 B | ✓ |
middleware.js gzip | 37.4 kB | 37.4 kB | N/A |
edge-runtime..pack.js gzip | 1.92 kB | 1.92 kB | ✓ |
Overall change | 2.07 kB | 2.07 kB | ✓ |
Next Runtimes
vercel/next.js canary | acdlite/next.js ppr-navigations-initial | Change | |
---|---|---|---|
app-page-exp...dev.js gzip | 168 kB | 168 kB | ✓ |
app-page-exp..prod.js gzip | 94.1 kB | 94.1 kB | ✓ |
app-page-tur..prod.js gzip | 94.8 kB | 94.8 kB | ✓ |
app-page-tur..prod.js gzip | 89.3 kB | 89.3 kB | ✓ |
app-page.run...dev.js gzip | 138 kB | 138 kB | ✓ |
app-page.run..prod.js gzip | 88.6 kB | 88.6 kB | ✓ |
app-route-ex...dev.js gzip | 24 kB | 24 kB | ✓ |
app-route-ex..prod.js gzip | 16.6 kB | 16.6 kB | ✓ |
app-route-tu..prod.js gzip | 16.6 kB | 16.6 kB | ✓ |
app-route-tu..prod.js gzip | 16.2 kB | 16.2 kB | ✓ |
app-route.ru...dev.js gzip | 23.4 kB | 23.4 kB | ✓ |
app-route.ru..prod.js gzip | 16.2 kB | 16.2 kB | ✓ |
pages-api-tu..prod.js gzip | 9.37 kB | 9.37 kB | ✓ |
pages-api.ru...dev.js gzip | 9.64 kB | 9.64 kB | ✓ |
pages-api.ru..prod.js gzip | 9.37 kB | 9.37 kB | ✓ |
pages-turbo...prod.js gzip | 21.9 kB | 21.9 kB | ✓ |
pages.runtim...dev.js gzip | 22.5 kB | 22.5 kB | ✓ |
pages.runtim..prod.js gzip | 21.9 kB | 21.9 kB | ✓ |
server.runti..prod.js gzip | 49.4 kB | 49.4 kB | ✓ |
Overall change | 930 kB | 930 kB | ✓ |
Diff details
Diff for page.js
Diff too large to display
Diff for 433-HASH.js
Diff too large to display
acdlite
force-pushed
the
ppr-navigations-initial
branch
5 times, most recently
from
December 18, 2023 06:43
d203d58
to
b9b9659
Compare
acdlite
commented
Dec 18, 2023
acdlite
force-pushed
the
ppr-navigations-initial
branch
5 times, most recently
from
December 19, 2023 18:01
7a61b11
to
58490cf
Compare
acdlite
changed the title
[WIP] PPR Navigations
Initial implementation of PPR client navigations
Dec 19, 2023
acdlite
requested review from
timneutkens,
ijjk,
shuding,
huozhi,
feedthejim,
ztanner and
wyattjoh
as code owners
December 19, 2023 18:03
ztanner
approved these changes
Dec 19, 2023
acdlite
force-pushed
the
ppr-navigations-initial
branch
from
December 20, 2023 03:03
58490cf
to
8ca1f80
Compare
acdlite
force-pushed
the
ppr-navigations-initial
branch
3 times, most recently
from
December 20, 2023 05:49
0622211
to
88fe459
Compare
acdlite
force-pushed
the
ppr-navigations-initial
branch
3 times, most recently
from
December 20, 2023 17:23
1526aee
to
8532444
Compare
For PPR, we will have two versions of the head: a prefetched version, and a full dynamic version. In preparation for this, I've updated findHeadInCache to return the entire Cache Node instead of just the `head` field, so in the future we can render either `head` or `prefetchHead`.
When PPR is enabled, there are two different versions of a segment's data we can render: a static version (that may contain holes for the dynamic parts) and a full version that contains everything. The static version is prefetched before navigation, and the full version is only loaded once the navigation occurs. Both versions are stored on the Cache Node object. Inside LayoutRouter, we must choose which version to render. We'll use an experimental feature of React's `useDeferredValue` hook: facebook/react#27500 ```js const dataToRender = useDeferredValue(fullData, prefetchedData) ``` React will coordinate when to switch from the prefetch data to the full data. For example, if the prefetch data is unable to finish rendering (this would happen if the segment contained dynamic data that was not wrapped in a Suspense boundary), it knows to switch to the full data even though the first render did not commit.
Like segment data, the head data for a segment may have two versions: a static version with dynamic holes, and a full version with everything. This adds a `prefetchHead` field to Cache Node. It works the same as the `prefetchRsc` field. As with the previous commit, we use `useDeferredValue` to determine which version to render. One awkward bit is that although the head element belongs to a particular segment, it's always rendered at the top of the App, by App Router. I believe this is to allow the page metadata to update even before the page itself appears. (Though I wonder if we could render this in the shared layout instead.) In the meantime, in order for useDeferredValue to work, we need to remount the head element whenever the segment changes.
For a more detailed explanation of the algorithm, refer to the comments in ppr-navigations.ts. Below is a high-level overview. Step 1: Render the prefetched data immediately Immediately upon navigation, we construct a new Cache Node tree (i.e. copy-on-write) that represents the optimistic result of a navigation, using both the current Cache Node tree and data that was prefetched prior to navigation. At this point, we haven't yet received the navigation response from the server. It could send back something completely different from the tree that was prefetched — due to rewrites, default routes, parallel routes, etc. But in most cases, it will return the same tree that we prefetched, just with the dynamic holes filled in. So we optimistically assume this will happen, and accept that the real result could be arbitrarily different. We'll reuse anything that was already in the previous tree, since that's what the server does. New segments (ones that don't appear in the old tree) are assigned an unresolved promise. The data for these promises will be fulfilled later, when the navigation response is received. The tree can be rendered immediately after it is created. Any new trees that do not have prefetch data will suspend during rendering, until the dynamic data streams in. Step 2: Fill in the dynamic data as it streams in When the dynamic data is received from the server, we can start filling in the unresolved promises in the tree. All the pending promises that were spawned by the navigation will be resolved, either with dynamic data from the server, or `null` to indicate that the data is missing. A `null` value will trigger a lazy fetch during render, which will then patch up the tree using the same mechanism as the non-PPR implementation (serverPatchReducer). Usually, the server will respond with exactly the subset of data that we're waiting for — everything below the nearest shared layout. But technically, the server can return anything it wants. This does _not_ create a new tree; it modifies the existing one in place. Which means it must follow the Suspense rules of cache safety.
acdlite
force-pushed
the
ppr-navigations-initial
branch
from
December 20, 2023 17:39
8532444
to
57285ca
Compare
acdlite
added a commit
to acdlite/next.js
that referenced
this pull request
Dec 20, 2023
In vercel#59725 I skipped this test in PPR prod mode, but not dev because CI wasn't failing for dev. The idea was to investigate the failure post-merge because it wasn't block-worthy. But the test did fail in dev mode when CI ran on canary. So this updates the guard to skip in dev, too. Will follow up with a PR to fix the test itself.
acdlite
added a commit
to acdlite/next.js
that referenced
this pull request
Dec 20, 2023
In vercel#59725 I skipped this test in PPR prod mode, but not dev because CI wasn't failing for dev. The idea was to investigate the failure post-merge because it wasn't block-worthy. But the test did fail in dev mode when CI ran on canary. So this updates the guard to skip in dev, too. Will follow up with a PR to fix the test itself.
acdlite
added a commit
that referenced
this pull request
Dec 20, 2023
In #59725 I skipped this test in PPR prod mode, but not dev because CI wasn't failing for dev. The idea was to investigate the failure post-merge because it wasn't block-worthy. But the test did fail in dev mode when CI ran on canary. So this updates the guard to skip in dev, too. Will follow up with a PR to fix the test itself. Closes NEXT-1913
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
For a more detailed explanation of the algorithm, refer to the comments in ppr-navigations.ts. Below is a high-level overview.
Step 1: Render the prefetched data immediately
Immediately upon navigation, we construct a new Cache Node tree (i.e. copy-on-write) that represents the optimistic result of a navigation, using both the current Cache Node tree and data that was prefetched prior to navigation.
At this point, we haven't yet received the navigation response from the server. It could send back something completely different from the tree that was prefetched — due to rewrites, default routes, parallel routes, etc.
But in most cases, it will return the same tree that we prefetched, just with the dynamic holes filled in. So we optimistically assume this will happen, and accept that the real result could be arbitrarily different.
We'll reuse anything that was already in the previous tree, since that's what the server does.
New segments (ones that don't appear in the old tree) are assigned an unresolved promise. The data for these promises will be fulfilled later, when the navigation response is received.
The tree can be rendered immediately after it is created. Any new trees that do not have prefetch data will suspend during rendering, until the dynamic data streams in.
Step 2: Fill in the dynamic data as it streams in
When the dynamic data is received from the server, we can start filling in the unresolved promises in the tree. All the pending promises that were spawned by the navigation will be resolved, either with dynamic data from the server, or
null
to indicate that the data is missing.A
null
value will trigger a lazy fetch during render, which will then patch up the tree using the same mechanism as the non-PPR implementation (serverPatchReducer).Usually, the server will respond with exactly the subset of data that we're waiting for — everything below the nearest shared layout. But technically, the server can return anything it wants.
This does not create a new tree; it modifies the existing one in place. Which means it must follow the Suspense rules of cache safety.
To Do
Not all necessarily PR-blocking, since the status quo is that navigations don't work at all when PPR is enabled
Closes NEXT-1894