Skip to content

Commit

Permalink
Partial Prerendering (#57287)
Browse files Browse the repository at this point in the history
This PR introduces a build optimization to create a "partial prerender" of the page.

1. During compilation, we create a static shell for the page using your existing Suspense boundaries. Components that can be static will be included in this static shell, leaving holes for the dynamic components.
1. Using `<Suspense />`, we can define fallbacks to be included in the partial prerender, as well as the holes for the dynamic components to stream into.

This means Next.js can initially serve a static loading skeleton, kicking off the dynamic parts in parallel. Then, the dynamic components stream in on demand. Dynamic components can use `cookies()`, `headers()`, `'cache': 'no-store'`, or `unstable_noStore()` to opt-into dynamic rendering.

Co-authored-by: Zack Tanner <1939140+ztanner@users.noreply.github.com>
  • Loading branch information
wyattjoh and ztanner authored Oct 24, 2023
1 parent 679a398 commit 69388a5
Show file tree
Hide file tree
Showing 52 changed files with 1,190 additions and 283 deletions.
5 changes: 5 additions & 0 deletions packages/next-swc/crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,11 @@ impl NextConfig {
self.await?.experimental.server_actions.unwrap_or(false),
))
}

#[turbo_tasks::function]
pub async fn enable_ppr(self: Vc<Self>) -> Result<Vc<bool>> {
Ok(Vc::cell(self.await?.experimental.ppr.unwrap_or(false)))
}
}

fn next_configs() -> Vc<Vec<String>> {
Expand Down
35 changes: 33 additions & 2 deletions packages/next-swc/crates/next-core/src/next_import_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ pub async fn get_next_client_import_map(
);
}
ClientContextType::App { app_dir } => {
let react_flavor = if *next_config.enable_server_actions().await? {
let react_flavor = if *next_config.enable_server_actions().await?
|| *next_config.enable_ppr().await?
{
"-experimental"
} else {
""
Expand All @@ -119,6 +121,27 @@ pub async fn get_next_client_import_map(
&format!("next/dist/compiled/react-dom{react_flavor}"),
),
);
import_map.insert_exact_alias(
"react-dom/static",
request_to_import_mapping(
app_dir,
"next/dist/compiled/react-dom-experimental/static",
),
);
import_map.insert_exact_alias(
"react-dom/static.edge",
request_to_import_mapping(
app_dir,
"next/dist/compiled/react-dom-experimental/static.edge",
),
);
import_map.insert_exact_alias(
"react-dom/static.browser",
request_to_import_mapping(
app_dir,
"next/dist/compiled/react-dom-experimental/static.browser",
),
);
import_map.insert_wildcard_alias(
"react-dom/",
request_to_import_mapping(
Expand Down Expand Up @@ -622,14 +645,22 @@ async fn rsc_aliases(
next_config: Vc<NextConfig>,
) -> Result<()> {
let server_actions = *next_config.enable_server_actions().await?;
let react_channel = if server_actions { "-experimental" } else { "" };
let ppr = *next_config.enable_ppr().await?;
let react_channel = if server_actions || ppr {
"-experimental"
} else {
""
};

let mut alias = indexmap! {
"react" => format!("next/dist/compiled/react{react_channel}"),
"react-dom" => format!("next/dist/compiled/react-dom{react_channel}"),
"react/jsx-runtime" => format!("next/dist/compiled/react{react_channel}/jsx-runtime"),
"react/jsx-dev-runtime" => format!("next/dist/compiled/react{react_channel}/jsx-dev-runtime"),
"react-dom/client" => format!("next/dist/compiled/react-dom{react_channel}/client"),
"react-dom/static" => format!("next/dist/compiled/react-dom-experimental/static"),
"react-dom/static.edge" => format!("next/dist/compiled/react-dom-experimental/static.edge"),
"react-dom/static.browser" => format!("next/dist/compiled/react-dom-experimental/static.browser"),
"react-dom/server" => format!("next/dist/compiled/react-dom{react_channel}/server"),
"react-dom/server.edge" => format!("next/dist/compiled/react-dom{react_channel}/server.edge"),
"react-dom/server.browser" => format!("next/dist/compiled/react-dom{react_channel}/server.browser"),
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/build/create-compiler-aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ export function createRSCAliases(
'react/jsx-dev-runtime$': `next/dist/compiled/react${bundledReactChannel}/jsx-dev-runtime`,
'react-dom/client$': `next/dist/compiled/react-dom${bundledReactChannel}/client`,
'react-dom/server$': `next/dist/compiled/react-dom${bundledReactChannel}/server`,
'react-dom/static$': `next/dist/compiled/react-dom-experimental/static`,
'react-dom/static.edge$': `next/dist/compiled/react-dom-experimental/static.edge`,
'react-dom/static.browser$': `next/dist/compiled/react-dom-experimental/static.browser`,
'react-dom/server.edge$': `next/dist/compiled/react-dom${bundledReactChannel}/server.edge`,
'react-dom/server.browser$': `next/dist/compiled/react-dom${bundledReactChannel}/server.browser`,
'react-server-dom-webpack/client$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client`,
Expand Down
46 changes: 29 additions & 17 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,7 @@ export default async function build(
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
nextConfigOutput: config.output,
ppr: config.experimental.ppr === true,
})
)

Expand Down Expand Up @@ -1459,7 +1460,7 @@ export default async function build(
})
return checkPageSpan.traceAsyncFn(async () => {
const actualPage = normalizePagePath(page)
const [selfSize, allSize] = await getJsPageSizeInKb(
const [size, totalSize] = await getJsPageSizeInKb(
pageType,
actualPage,
distDir,
Expand All @@ -1469,7 +1470,8 @@ export default async function build(
computedManifestData
)

let isSsg = false
let isPPR = false
let isSSG = false
let isStatic = false
let isServerComponent = false
let isHybridAmp = false
Expand Down Expand Up @@ -1579,6 +1581,7 @@ export default async function build(
maxMemoryCacheSize:
config.experimental.isrMemoryCacheSize,
nextConfigOutput: config.output,
ppr: config.experimental.ppr === true,
})
}
)
Expand All @@ -1588,12 +1591,21 @@ export default async function build(
// TODO-APP: handle prerendering with edge
if (isEdgeRuntime(pageRuntime)) {
isStatic = false
isSsg = false
isSSG = false

Log.warnOnce(
`Using edge runtime on a page currently disables static generation for that page`
)
} else {
// If this route can be partially pre-rendered, then
// mark it as such and mark it that it can be
// generated server-side.
if (workerResult.isPPR) {
isPPR = workerResult.isPPR
isSSG = true
isStatic = true
}

if (
workerResult.encodedPrerenderRoutes &&
workerResult.prerenderRoutes
Expand All @@ -1607,7 +1619,7 @@ export default async function build(
workerResult.encodedPrerenderRoutes
)
ssgPageRoutes = workerResult.prerenderRoutes
isSsg = true
isSSG = true
}

const appConfig = workerResult.appConfig || {}
Expand Down Expand Up @@ -1694,7 +1706,7 @@ export default async function build(

if (workerResult.hasStaticProps) {
ssgPages.add(page)
isSsg = true
isSSG = true

if (
workerResult.prerenderRoutes &&
Expand Down Expand Up @@ -1729,7 +1741,7 @@ export default async function build(
// This is a static server component page that doesn't have
// gSP or gSSP. We still treat it as a SSG page.
ssgPages.add(page)
isSsg = true
isSSG = true
}

if (hasPages404 && page === '/404') {
Expand Down Expand Up @@ -1772,7 +1784,7 @@ export default async function build(
}

if (pageType === 'app') {
if (isSsg || isStatic) {
if (isSSG || isStatic) {
staticAppPagesCount++
} else {
serverAppPagesCount++
Expand All @@ -1781,10 +1793,11 @@ export default async function build(
}

pageInfos.set(page, {
size: selfSize,
totalSize: allSize,
static: isStatic,
isSsg,
size,
totalSize,
isStatic,
isSSG,
isPPR,
isHybridAmp,
ssgPageRoutes,
initialRevalidateSeconds: false,
Expand Down Expand Up @@ -2191,14 +2204,13 @@ export default async function build(
appConfig.revalidate === 0 ||
exportResult.byPath.get(page)?.revalidate === 0

// TODO: (wyattjoh) maybe change behavior for postpone?
if (hasDynamicData && pageInfos.get(page)?.static) {
if (hasDynamicData && pageInfos.get(page)?.isStatic) {
// if the page was marked as being static, but it contains dynamic data
// (ie, in the case of a static generation bailout), then it should be marked dynamic
pageInfos.set(page, {
...(pageInfos.get(page) as PageInfo),
static: false,
isSsg: false,
isStatic: false,
isSSG: false,
})
}

Expand Down Expand Up @@ -2274,8 +2286,8 @@ export default async function build(
// used dynamic data
pageInfos.set(route, {
...(pageInfos.get(route) as PageInfo),
isSsg: false,
static: false,
isSSG: false,
isStatic: false,
})
}
})
Expand Down
Loading

0 comments on commit 69388a5

Please sign in to comment.