Skip to content
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

Merged
merged 14 commits into from
Aug 28, 2024
Merged

Partial Fallback Prerendering #68958

merged 14 commits into from
Aug 28, 2024

Conversation

wyattjoh
Copy link
Member

@wyattjoh wyattjoh commented Aug 15, 2024

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 or loading.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:

// /app/users/[userID]/page.jsx
import { Suspense } from 'react'

function Profile({ params }) {
  const { userID } = params
  return <div>Hello {userID}!</div>
}

export default function ProfilePage({ params }) {
  return (
    <div>
      <h1>User Profile</h1>
      <Suspense fallback="Loading...">
        <Profile params={params} />
      </Suspense>
    </div>
  )
}

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 a loading.jsx at a segment will automatically wrap the page.jsx with a suspense boundary, setting the contents of the root loading.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:

module.exports = {
  experimental: {
    ppr: true,
    pprFallbacks: true,
  }
}

Once PFPR has stabilized with hosting providers, the experimental flag will go away and it will become the default with the PPR flag.

Copy link
Member Author

wyattjoh commented Aug 15, 2024

This stack of pull requests is managed by Graphite. Learn more about stacking.

Join @wyattjoh and the rest of your teammates on Graphite Graphite

@ijjk
Copy link
Member

ijjk commented Aug 15, 2024

Tests Passed

@ijjk
Copy link
Member

ijjk commented Aug 15, 2024

Stats from current PR

Default Build (Increase detected ⚠️)
General Overall increase ⚠️
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 ⚠️ +624 kB
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 ⚠️ +4.09 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 ⚠️ +4.09 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 ⚠️ +345 B
page.js gzip 173 kB 176 kB ⚠️ +2.62 kB
Overall change 301 kB 304 kB ⚠️ +2.97 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 ⚠️ +1.17 kB
app-page-exp..prod.js gzip 123 kB 123 kB ⚠️ +675 B
app-page-tur..prod.js gzip 136 kB 137 kB ⚠️ +683 B
app-page-tur..prod.js gzip 131 kB 132 kB ⚠️ +700 B
app-page.run...dev.js gzip 301 kB 302 kB ⚠️ +1.16 kB
app-page.run..prod.js gzip 118 kB 119 kB ⚠️ +694 B
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 ⚠️ +856 B
Overall change 1.28 MB 1.28 MB ⚠️ +5.93 kB
build cache Overall increase ⚠️
vercel/next.js canary vercel/next.js feat/pfpr Change
0.pack gzip 1.49 MB 1.56 MB ⚠️ +67.3 kB
index.pack gzip 127 kB 129 kB ⚠️ +2.49 kB
Overall change 1.62 MB 1.69 MB ⚠️ +69.8 kB
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

Commit: d6bb4e9

Comment on lines 11 to 26
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
}
Copy link
Contributor

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') {
Copy link
Contributor

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

packages/next/src/client/components/navigation.ts Outdated Show resolved Hide resolved
packages/next/src/client/components/params.ts Outdated Show resolved Hide resolved
packages/next/src/client/components/params.ts Outdated Show resolved Hide resolved
packages/next/src/client/components/params.ts Outdated Show resolved Hide resolved
packages/next/src/client/components/params.ts Outdated Show resolved Hide resolved
packages/next/src/server/app-render/app-render.tsx Outdated Show resolved Hide resolved
packages/next/src/server/app-render/app-render.tsx Outdated Show resolved Hide resolved
@wyattjoh wyattjoh changed the base branch from refactor/fallback-incremental-cache to graphite-base/68958 August 20, 2024 20:33
wyattjoh added a commit that referenced this pull request Aug 20, 2024
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.
const page = appNormalizedPaths.get(originalAppPath) || ''
const appConfig = appDefaultConfigs.get(originalAppPath) || {}
let hasDynamicData =

let hasRevalidateZero =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I found the original name a bit clearer since we use a revalidation of 0 synonymously with dynamic access. The significance of this check becomes lost and it creates a bit of confusion with later comments (ie, right below, "if the page was marked as being static but it contains dynamic data..."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a PPR world though, we don't actually use the revalidate: 0 to indicate if a page has dynamic data or not, we simply postpone. Renaming this here adds additional clarity so lower in the code it's clearer what we're comparing directly.

packages/next/src/build/index.ts Outdated Show resolved Hide resolved
@wyattjoh wyattjoh force-pushed the feat/pfpr branch 3 times, most recently from 8bcc38e to 01b0828 Compare August 27, 2024 22:28
@wyattjoh wyattjoh changed the base branch from canary to fix/safari-firefox-test-modes August 27, 2024 22:28
Base automatically changed from fix/safari-firefox-test-modes to canary August 27, 2024 22:35
@wyattjoh wyattjoh force-pushed the feat/pfpr branch 3 times, most recently from 7a13005 to d6bb4e9 Compare August 27, 2024 22:37
@wyattjoh wyattjoh merged commit f4c1a40 into canary Aug 28, 2024
103 of 109 checks passed
@wyattjoh wyattjoh deleted the feat/pfpr branch August 28, 2024 05:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants