-
Notifications
You must be signed in to change notification settings - Fork 27.8k
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
Partial Fallback Prerendering #68958
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Tests Passed |
Stats from current PRDefault Build (Increase detected
|
vercel/next.js canary | vercel/next.js feat/pfpr | Change | |
---|---|---|---|
buildDuration | 17.2s | 15.5s | N/A |
buildDurationCached | 8.5s | 7.2s | N/A |
nodeModulesSize | 355 MB | 356 MB | |
nextStartRea..uration (ms) | 428ms | 438ms | N/A |
Client Bundles (main, webpack) Overall increase ⚠️
vercel/next.js canary | vercel/next.js feat/pfpr | Change | |
---|---|---|---|
2994-HASH.js gzip | 37.8 kB | 41.9 kB | |
3095.HASH.js gzip | 169 B | 169 B | ✓ |
9630-HASH.js gzip | 5.25 kB | 5.25 kB | N/A |
a8273233-HASH.js gzip | 51.9 kB | 51.9 kB | N/A |
framework-HASH.js gzip | 56.7 kB | 56.7 kB | N/A |
main-app-HASH.js gzip | 226 B | 225 B | N/A |
main-HASH.js gzip | 32.5 kB | 32.5 kB | N/A |
webpack-HASH.js gzip | 1.71 kB | 1.71 kB | N/A |
Overall change | 37.9 kB | 42 kB |
Legacy Client Bundles (polyfills)
vercel/next.js canary | vercel/next.js feat/pfpr | Change | |
---|---|---|---|
polyfills-HASH.js gzip | 31 kB | 31 kB | ✓ |
Overall change | 31 kB | 31 kB | ✓ |
Client Pages
vercel/next.js canary | vercel/next.js feat/pfpr | Change | |
---|---|---|---|
_app-HASH.js gzip | 194 B | 193 B | N/A |
_error-HASH.js gzip | 192 B | 192 B | ✓ |
amp-HASH.js gzip | 508 B | 511 B | N/A |
css-HASH.js gzip | 344 B | 342 B | N/A |
dynamic-HASH.js gzip | 1.84 kB | 1.84 kB | N/A |
edge-ssr-HASH.js gzip | 265 B | 266 B | N/A |
head-HASH.js gzip | 364 B | 364 B | ✓ |
hooks-HASH.js gzip | 390 B | 390 B | ✓ |
image-HASH.js gzip | 4.4 kB | 4.4 kB | N/A |
index-HASH.js gzip | 267 B | 268 B | N/A |
link-HASH.js gzip | 2.81 kB | 2.81 kB | N/A |
routerDirect..HASH.js gzip | 328 B | 328 B | ✓ |
script-HASH.js gzip | 397 B | 396 B | N/A |
withRouter-HASH.js gzip | 323 B | 324 B | N/A |
1afbb74e6ecf..834.css gzip | 106 B | 106 B | ✓ |
Overall change | 1.38 kB | 1.38 kB | ✓ |
Client Build Manifests
vercel/next.js canary | vercel/next.js feat/pfpr | Change | |
---|---|---|---|
_buildManifest.js gzip | 750 B | 748 B | N/A |
Overall change | 0 B | 0 B | ✓ |
Rendered Page Sizes
vercel/next.js canary | vercel/next.js feat/pfpr | Change | |
---|---|---|---|
index.html gzip | 522 B | 522 B | ✓ |
link.html gzip | 538 B | 536 B | N/A |
withRouter.html gzip | 519 B | 517 B | N/A |
Overall change | 522 B | 522 B | ✓ |
Edge SSR bundle Size Overall increase ⚠️
vercel/next.js canary | vercel/next.js feat/pfpr | Change | |
---|---|---|---|
edge-ssr.js gzip | 127 kB | 128 kB | |
page.js gzip | 173 kB | 176 kB | |
Overall change | 301 kB | 304 kB |
Middleware size
vercel/next.js canary | vercel/next.js feat/pfpr | Change | |
---|---|---|---|
middleware-b..fest.js gzip | 669 B | 673 B | N/A |
middleware-r..fest.js gzip | 156 B | 156 B | ✓ |
middleware.js gzip | 29.9 kB | 29.9 kB | N/A |
edge-runtime..pack.js gzip | 844 B | 844 B | ✓ |
Overall change | 1 kB | 1 kB | ✓ |
Next Runtimes Overall increase ⚠️
vercel/next.js canary | vercel/next.js feat/pfpr | Change | |
---|---|---|---|
928-experime...dev.js gzip | 322 B | 322 B | ✓ |
928.runtime.dev.js gzip | 314 B | 314 B | ✓ |
app-page-exp...dev.js gzip | 312 kB | 313 kB | |
app-page-exp..prod.js gzip | 123 kB | 123 kB | |
app-page-tur..prod.js gzip | 136 kB | 137 kB | |
app-page-tur..prod.js gzip | 131 kB | 132 kB | |
app-page.run...dev.js gzip | 301 kB | 302 kB | |
app-page.run..prod.js gzip | 118 kB | 119 kB | |
app-route-ex...dev.js gzip | 30.8 kB | 30.8 kB | N/A |
app-route-ex..prod.js gzip | 20.8 kB | 20.8 kB | N/A |
app-route-tu..prod.js gzip | 20.8 kB | 20.8 kB | N/A |
app-route-tu..prod.js gzip | 20.6 kB | 20.7 kB | N/A |
app-route.ru...dev.js gzip | 32.4 kB | 32.5 kB | N/A |
app-route.ru..prod.js gzip | 20.6 kB | 20.7 kB | N/A |
pages-api-tu..prod.js gzip | 9.62 kB | 9.62 kB | ✓ |
pages-api.ru...dev.js gzip | 11.5 kB | 11.5 kB | ✓ |
pages-api.ru..prod.js gzip | 9.61 kB | 9.61 kB | ✓ |
pages-turbo...prod.js gzip | 20.8 kB | 20.8 kB | ✓ |
pages.runtim...dev.js gzip | 26.4 kB | 26.4 kB | ✓ |
pages.runtim..prod.js gzip | 20.8 kB | 20.8 kB | ✓ |
server.runti..prod.js gzip | 56.8 kB | 57.6 kB | |
Overall change | 1.28 MB | 1.28 MB |
build cache Overall increase ⚠️
vercel/next.js canary | vercel/next.js feat/pfpr | Change | |
---|---|---|---|
0.pack gzip | 1.49 MB | 1.56 MB | |
index.pack gzip | 127 kB | 129 kB | |
Overall change | 1.62 MB | 1.69 MB |
Diff details
Diff for page.js
Diff too large to display
Diff for middleware.js
Diff too large to display
Diff for edge-ssr.js
Diff too large to display
Diff for image-HASH.js
@@ -1,7 +1,7 @@
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
[8358],
{
- /***/ 6682: /***/ (
+ /***/ 8480: /***/ (
__unused_webpack_module,
__unused_webpack_exports,
__webpack_require__
@@ -9,7 +9,7 @@
(window.__NEXT_P = window.__NEXT_P || []).push([
"/image",
function () {
- return __webpack_require__(1471);
+ return __webpack_require__(3908);
},
]);
if (false) {
@@ -18,7 +18,7 @@
/***/
},
- /***/ 2968: /***/ (module, exports, __webpack_require__) => {
+ /***/ 9178: /***/ (module, exports, __webpack_require__) => {
"use strict";
/* __next_internal_client_entry_do_not_use__ cjs */
Object.defineProperty(exports, "__esModule", {
@@ -40,17 +40,17 @@
__webpack_require__(5550)
);
const _head = /*#__PURE__*/ _interop_require_default._(
- __webpack_require__(9973)
+ __webpack_require__(7261)
);
- const _getimgprops = __webpack_require__(3971);
- const _imageconfig = __webpack_require__(985);
- const _imageconfigcontextsharedruntime = __webpack_require__(7496);
- const _warnonce = __webpack_require__(9125);
- const _routercontextsharedruntime = __webpack_require__(9390);
+ const _getimgprops = __webpack_require__(5184);
+ const _imageconfig = __webpack_require__(5055);
+ const _imageconfigcontextsharedruntime = __webpack_require__(9427);
+ const _warnonce = __webpack_require__(3435);
+ const _routercontextsharedruntime = __webpack_require__(3626);
const _imageloader = /*#__PURE__*/ _interop_require_default._(
- __webpack_require__(855)
+ __webpack_require__(7510)
);
- const _usemergedref = __webpack_require__(752);
+ const _usemergedref = __webpack_require__(3782);
// This is replaced by webpack define plugin
const configEnv = {
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
@@ -371,7 +371,7 @@
/***/
},
- /***/ 752: /***/ (module, exports, __webpack_require__) => {
+ /***/ 3782: /***/ (module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -440,7 +440,7 @@
/***/
},
- /***/ 3971: /***/ (
+ /***/ 5184: /***/ (
__unused_webpack_module,
exports,
__webpack_require__
@@ -456,9 +456,9 @@
return getImgProps;
},
});
- const _warnonce = __webpack_require__(9125);
- const _imageblursvg = __webpack_require__(5602);
- const _imageconfig = __webpack_require__(985);
+ const _warnonce = __webpack_require__(3435);
+ const _imageblursvg = __webpack_require__(2564);
+ const _imageconfig = __webpack_require__(5055);
const VALID_LOADING_VALUES =
/* unused pure expression or super */ null && [
"lazy",
@@ -830,7 +830,7 @@
/***/
},
- /***/ 5602: /***/ (__unused_webpack_module, exports) => {
+ /***/ 2564: /***/ (__unused_webpack_module, exports) => {
"use strict";
/**
* A shared function, used on both client and server, to generate a SVG blur placeholder.
@@ -885,7 +885,7 @@
/***/
},
- /***/ 1585: /***/ (
+ /***/ 1146: /***/ (
__unused_webpack_module,
exports,
__webpack_require__
@@ -912,10 +912,10 @@
},
});
const _interop_require_default = __webpack_require__(4345);
- const _getimgprops = __webpack_require__(3971);
- const _imagecomponent = __webpack_require__(2968);
+ const _getimgprops = __webpack_require__(5184);
+ const _imagecomponent = __webpack_require__(9178);
const _imageloader = /*#__PURE__*/ _interop_require_default._(
- __webpack_require__(855)
+ __webpack_require__(7510)
);
function getImageProps(imgProps) {
const { props } = (0, _getimgprops.getImgProps)(imgProps, {
@@ -947,7 +947,7 @@
/***/
},
- /***/ 855: /***/ (__unused_webpack_module, exports) => {
+ /***/ 7510: /***/ (__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -982,7 +982,7 @@
/***/
},
- /***/ 1471: /***/ (
+ /***/ 3908: /***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
@@ -999,8 +999,8 @@
// EXTERNAL MODULE: ./node_modules/.pnpm/react@19.0.0-rc-eb3ad065-20240822/node_modules/react/jsx-runtime.js
var jsx_runtime = __webpack_require__(3801);
- // EXTERNAL MODULE: ./node_modules/.pnpm/next@file+..+main-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-eb3ad065-20240822_re_ixk2rr5gnwbjcqqikyozc23shi/node_modules/next/image.js
- var next_image = __webpack_require__(7764);
+ // EXTERNAL MODULE: ./node_modules/.pnpm/next@file+..+diff-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-eb3ad065-20240822_re_73fog5rwwduhvoesnuzrxcpoki/node_modules/next/image.js
+ var next_image = __webpack_require__(101);
var image_default = /*#__PURE__*/ __webpack_require__.n(next_image); // CONCATENATED MODULE: ./pages/nextjs.png
/* harmony default export */ const nextjs = {
src: "/_next/static/media/nextjs.cae0b805.png",
@@ -1030,12 +1030,12 @@
/***/
},
- /***/ 7764: /***/ (
+ /***/ 101: /***/ (
module,
__unused_webpack_exports,
__webpack_require__
) => {
- module.exports = __webpack_require__(1585);
+ module.exports = __webpack_require__(1146);
/***/
},
@@ -1045,7 +1045,7 @@
/******/ var __webpack_exec__ = (moduleId) =>
__webpack_require__((__webpack_require__.s = moduleId));
/******/ __webpack_require__.O(0, [2888, 9774, 179], () =>
- __webpack_exec__(6682)
+ __webpack_exec__(8480)
);
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ _N_E = __webpack_exports__;
Diff for 2994-HASH.js
Diff too large to display
Diff for main-HASH.js
Diff too large to display
Diff for app-page-exp..ntime.dev.js
failed to diff
Diff for app-page-exp..time.prod.js
Diff too large to display
Diff for app-page-tur..time.prod.js
Diff too large to display
Diff for app-page-tur..time.prod.js
Diff too large to display
Diff for app-page.runtime.dev.js
Diff too large to display
Diff for app-page.runtime.prod.js
Diff too large to display
Diff for app-route-ex..ntime.dev.js
Diff too large to display
Diff for app-route-ex..time.prod.js
Diff too large to display
Diff for app-route-tu..time.prod.js
Diff too large to display
Diff for app-route-tu..time.prod.js
Diff too large to display
Diff for app-route.runtime.dev.js
Diff too large to display
Diff for app-route.ru..time.prod.js
Diff too large to display
Diff for server.runtime.prod.js
Diff too large to display
6ed3d1f
to
5aae257
Compare
f12d23d
to
6268654
Compare
8504f98
to
5c9de13
Compare
fc23b9e
to
2585be0
Compare
5c9de13
to
0b566b7
Compare
0b566b7
to
e44ee41
Compare
function hasUnknownRouteParams() { | ||
if (typeof window === 'undefined') { | ||
// AsyncLocalStorage should not be included in the client bundle. | ||
const { staticGenerationAsyncStorage } = | ||
require('./static-generation-async-storage.external') as typeof import('./static-generation-async-storage.external') | ||
|
||
const staticGenerationStore = staticGenerationAsyncStorage.getStore() | ||
if (!staticGenerationStore) return false | ||
|
||
const { unknownRouteParams } = staticGenerationStore | ||
return isUnknownRouteParams(unknownRouteParams) | ||
} | ||
|
||
return false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe put this all in an RSC scope and pass something to a client Context that way you don't need the window check. during prerender it would indicate there are unknown route params and during resume/dynamic render it would not.
Still need to make sure there are not mismatches between prerender and resume but that's also true in the current setup so you presumably are already handling that
@@ -63,6 +69,26 @@ export function useSearchParams(): ReadonlyURLSearchParams { | |||
return readonlySearchParams | |||
} | |||
|
|||
function trackParamsAccessed(expression: string) { | |||
if (typeof window === 'undefined') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we're just begging for import map support for this kind of thing. Ideally we have an implementation for the browser and a separate one for SSR. This one can't as easily be pulled out of the client scope because it actually has to call a function which reads from the async local storage
This replaces the custom behaviour of `getFallback` in the server with the existing ResponseCache. This sets us up for #68958 which has fallbacks that should be revalidatable.
e44ee41
to
cc484b9
Compare
7a13005
to
d6bb4e9
Compare
…anifest, improved serialization speed
…d comments, remove server code from client bundles
…arsing, updated serialization format
Building on Partial Fallback Prerendering (#68958), this expands the behaviour after a fallback shell is served. Once a fallback is served, the route shell is then generated. Future requests will then use this new route shell instead.
This work introduces the new concept of Partial Fallback Prerendering (PFPR).
Traditionally, when a dynamic page needed to be routed to that wasn't pregenerated, it required a render to generate even the first few bytes of the static page itself. This resulted in slow page loads for pages not frequently visited and a reduced Time to First Byte (TTFB) score on Core Web Vitals (CWV).
PFPR takes advantage of the new systems of Partial Prerendering (PPR) that allows the application to suspend at different points mid-render, and resume it later. We mark any unknown parameter access as dynamic access, and suspend the rendering up to the next suspense boundaries at those points. Under ideal conditions (correctly placed
<Suspense />
boundaries orloading.jsx
files) this generates a static shell that can be served to users as soon as the request hits Next.js, right out of the static cache. This minimizes the TTFB for all requests, dynamic or not for those pages that enable PPR. For example, the following page would create a usable shell:Due to the way that suspense works within React components, access of params within the root page component would cause the whole page to suspend. Thankfully, that's where the
loading.jsx
comes in handy. Adding aloading.jsx
at a segment will automatically wrap thepage.jsx
with a suspense boundary, setting the contents of the rootloading.jsx
as the fallback component to use for it. This lets you maintain your existing style of accessing parameters at the root of the components while also taking advantage of PFPR.To enable this feature, you first need to enable both PPR and PFPR:
Once PFPR has stabilized with hosting providers, the experimental flag will go away and it will become the default with the PPR flag.