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

Build and generate cleanup #59420

Closed
wants to merge 1 commit into from
Closed

Conversation

wyattjoh
Copy link
Member

@wyattjoh wyattjoh commented Dec 8, 2023

This is taking a pass at strengthening the Typescript types and organization of some of the build and generate steps. This is aligned with improving Partial Pre-Rendering (PPR) support as it delegates detection of PPR to the static analysis/generation stage. This ensures that when we want to tweak the logic of what pages are configured for PPR, that we only need to do so in a single place.

This also added some runtime checks for values to prevent unexpected failures.

Existing test suites are sufficient to verify the new behavior.

Closes NEXT-1834

@ijjk ijjk added created-by: Next.js team PRs by the Next.js team. type: next labels Dec 8, 2023
@wyattjoh
Copy link
Member Author

wyattjoh commented Dec 8, 2023

Current dependencies on/for this PR:

This stack of pull requests is managed by Graphite.

@ijjk
Copy link
Member

ijjk commented Dec 8, 2023

Tests Passed

@wyattjoh wyattjoh force-pushed the refactor/build-info-cleanup branch 2 times, most recently from 040358f to dde8b6f Compare December 8, 2023 21:32
@ijjk
Copy link
Member

ijjk commented Dec 8, 2023

Stats from current PR

Default Build
General Overall increase ⚠️
vercel/next.js canary vercel/next.js refactor/build-info-cleanup Change
buildDuration 10.8s 10.7s N/A
buildDurationCached 6s 5.9s N/A
nodeModulesSize 199 MB 199 MB ⚠️ +19.9 kB
nextStartRea..uration (ms) 423ms 423ms
Client Bundles (main, webpack)
vercel/next.js canary vercel/next.js refactor/build-info-cleanup Change
170-HASH.js gzip 26.7 kB 26.7 kB N/A
199.HASH.js gzip 181 B 185 B N/A
3f784ff6-HASH.js gzip 53.3 kB 53.3 kB
framework-HASH.js gzip 45.2 kB 45.2 kB
main-app-HASH.js gzip 242 B 243 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 98.5 kB 98.5 kB
Legacy Client Bundles (polyfills)
vercel/next.js canary vercel/next.js refactor/build-info-cleanup Change
polyfills-HASH.js gzip 31 kB 31 kB
Overall change 31 kB 31 kB
Client Pages
vercel/next.js canary vercel/next.js refactor/build-info-cleanup Change
_app-HASH.js gzip 195 B 194 B N/A
_error-HASH.js gzip 183 B 182 B N/A
amp-HASH.js gzip 501 B 501 B
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 255 B
head-HASH.js gzip 349 B 350 B N/A
hooks-HASH.js gzip 368 B 369 B N/A
image-HASH.js gzip 4.27 kB 4.27 kB N/A
index-HASH.js gzip 255 B 256 B N/A
link-HASH.js gzip 2.6 kB 2.6 kB N/A
routerDirect..HASH.js gzip 311 B 309 B N/A
script-HASH.js gzip 384 B 384 B
withRouter-HASH.js gzip 307 B 306 B N/A
1afbb74e6ecf..834.css gzip 106 B 106 B
Overall change 1.57 kB 1.57 kB
Client Build Manifests
vercel/next.js canary vercel/next.js refactor/build-info-cleanup Change
_buildManifest.js gzip 483 B 483 B
Overall change 483 B 483 B
Rendered Page Sizes
vercel/next.js canary vercel/next.js refactor/build-info-cleanup Change
index.html gzip 529 B 529 B
link.html gzip 542 B 542 B
withRouter.html gzip 524 B 524 B
Overall change 1.59 kB 1.59 kB
Edge SSR bundle Size
vercel/next.js canary vercel/next.js refactor/build-info-cleanup Change
edge-ssr.js gzip 93.5 kB 93.6 kB N/A
page.js gzip 146 kB 146 kB N/A
Overall change 0 B 0 B
Middleware size
vercel/next.js canary vercel/next.js refactor/build-info-cleanup Change
middleware-b..fest.js gzip 622 B 622 B
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.7 kB 2.7 kB
Next Runtimes
vercel/next.js canary vercel/next.js refactor/build-info-cleanup Change
app-page-exp...dev.js gzip 168 kB 168 kB N/A
app-page-exp..prod.js gzip 93.9 kB 94 kB N/A
app-page-tur..prod.js gzip 94.7 kB 94.7 kB N/A
app-page-tur..prod.js gzip 89.2 kB 89.3 kB N/A
app-page.run...dev.js gzip 138 kB 138 kB N/A
app-page.run..prod.js gzip 88.6 kB 88.6 kB N/A
app-route-ex...dev.js gzip 24 kB 24 kB
app-route-ex..prod.js gzip 16.7 kB 16.7 kB
app-route-tu..prod.js gzip 16.7 kB 16.7 kB
app-route-tu..prod.js gzip 16.3 kB 16.3 kB
app-route.ru...dev.js gzip 23.4 kB 23.4 kB
app-route.ru..prod.js gzip 16.3 kB 16.3 kB
pages-api-tu..prod.js gzip 9.37 kB 9.4 kB N/A
pages-api.ru...dev.js gzip 9.64 kB 9.67 kB N/A
pages-api.ru..prod.js gzip 9.37 kB 9.39 kB N/A
pages-turbo...prod.js gzip 21.9 kB 22 kB N/A
pages.runtim...dev.js gzip 22.6 kB 22.6 kB N/A
pages.runtim..prod.js gzip 21.9 kB 22 kB N/A
server.runti..prod.js gzip 49.4 kB 49.4 kB N/A
Overall change 113 kB 113 kB
Diff details
Diff for page.js

Diff too large to display

Diff for edge-ssr.js

Diff too large to display

Diff for 170-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 pages-api-tu..time.prod.js

Diff too large to display

Diff for pages-api.runtime.dev.js

Diff too large to display

Diff for pages-api.ru..time.prod.js

Diff too large to display

Diff for pages-turbo...time.prod.js

Diff too large to display

Diff for pages.runtime.dev.js

Diff too large to display

Diff for pages.runtime.prod.js

Diff too large to display

Diff for server.runtime.prod.js

Diff too large to display

Commit: e9e957e

@wyattjoh wyattjoh force-pushed the refactor/build-info-cleanup branch 3 times, most recently from 339fe87 to 2da50b5 Compare December 8, 2023 22:38
Comment on lines 353 to 379
function createAppDataRouteInfo(
page: string,
{
supportsPPR,
isRouteHandler,
}: {
supportsPPR: boolean
isRouteHandler: boolean
}
): DataRouteRouteInfo {
const normalizedRoute = normalizePagePath(page)

// If the page is not a route handler and the page is either doesn't support
// PPR or is not PPR, we need to generate a data route.
let dataRoute: string | null = null
if (!isRouteHandler) {
dataRoute = path.posix.join(`${normalizedRoute}${RSC_SUFFIX}`)
}

let prefetchDataRoute: string | undefined
if (supportsPPR) {
prefetchDataRoute = path.posix.join(
`${normalizedRoute}${RSC_PREFETCH_SUFFIX}`
)
}

return { dataRoute, prefetchDataRoute }
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Pulling this into it's own function de-duplicates this logic.

@@ -2373,33 +2390,27 @@ export default async function build(
hasDynamicData = true
// we might have determined during prerendering that this page
// used dynamic data
pageInfos.set(route, {
...(pageInfos.get(route) as PageInfo),
pageInfoRegistry.patch(route, {
Copy link
Member Author

Choose a reason for hiding this comment

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

This .patch method checks if the underlying page exists first, otherwise it throws. This ensures that we don't put the registry into an invalid state.

@@ -1253,7 +1296,7 @@ export async function buildAppStaticPaths({
fetchCacheKeyPrefix?: string
maxMemoryCacheSize?: number
requestHeaders: IncrementalCache['requestHeaders']
ppr: boolean
nextConfigPPREnabled: boolean
Copy link
Member Author

Choose a reason for hiding this comment

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

The renaming here dramatically improves the clarity of the logic.

// this page statically unless PPR is enabled, in which case the static
// shell can always be prerendered.
if (appConfig.dynamic === 'force-dynamic') {
appConfig.revalidate = isPPR ? false : 0
Copy link
Member Author

Choose a reason for hiding this comment

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

When a page is going to be rendered using PPR, if force-dynamic is set on the segment config, the entire page at the root will postpone, resulting in a completly static (and safe) payload. This can be cached for lifetime of the build.

*/
const supportsPPR = !isRouteHandler && nextConfigPPREnabled

const experimentalPPR = isPPR ? true : undefined
Copy link
Member Author

Choose a reason for hiding this comment

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

We're now using the information yielded from the static analysis to determine if the page is static or not rather than duplicating the logic here.

const parentSegments: string[] = []

const stack: LoaderTree[] = [tree]
while (stack.length > 0) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This change replaced the recursion with a loop/stack.

Comment on lines +1 to +14
export function interopDefault<T = any>(mod: unknown): T | undefined {
// If the module is falsy (like `null`), just return undefined.
if (!mod) return undefined

// If the module has a default export (named 'default'), return that.
if (
(typeof mod === 'object' || typeof mod === 'function') &&
'default' in mod &&
mod.default
) {
return mod.default as T
}

return mod as T
Copy link
Member Author

Choose a reason for hiding this comment

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

The typing updates here improve the reliability of the return and add optional typescript types to the interopted modules.

I'm open to adjusting this to always return unknown, but the any return here was too dangerous and resulted in a bug.

Comment on lines +119 to +120
Document = interopDefault(DocumentMod)
App = interopDefault(AppMod)
Copy link
Member Author

Choose a reason for hiding this comment

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

These interoped values are now correctly including undefined, ensuring that downstream methods check the existence of the values prior to usage.

@@ -452,15 +452,17 @@ export async function renderToHTMLImpl(
runtime: globalRuntime,
isExperimentalCompile,
} = renderOpts

if (!renderOpts.Component) {
throw new Error('Invariant: Cannot render without a Component')
Copy link
Member Author

Choose a reason for hiding this comment

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

Invarients here ensure that we don't have mystery failures due to passing around an any which was undefined.

@wyattjoh wyattjoh marked this pull request as ready for review December 8, 2023 22:55
@gnoff
Copy link
Contributor

gnoff commented Dec 8, 2023

@wyattjoh are you able to reopen as separate PRs? It seems like there are 3 or 4 distinct refactors going on here and it would help me track them if we could keep them separate? For instance your explanatory comments basically make good PR descriptions

*/
public get(page: string, throwIfUndefined: true): PageInfo
public get(page: string, throwIfUndefined?: boolean): PageInfo | undefined
public get(page: string, throwIfUndefined?: boolean): PageInfo | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

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

If the callsite knows whether a throw is necessary then the callsite should just thrown when this map doesn't return anything

* build output.
*/

export class PageInfoRegistry extends Map<string, PageInfo> {
Copy link
Contributor

Choose a reason for hiding this comment

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

extending a built-in in this way IMO obfuscates what the registry can do. For instance if I see the type of the registry is a map I know I can do Map-like stuff to it but if I see it is some class I have to build a new mental model of it's some base thing + some additional capabilities.

I'd argue that this particular abstraction isn't valuable and a simple patchPageInfo function would suffice everyone we call registry.patch and a expectPageInfo() function would suffice if you want to throw on missing page data. Then the other places where you interact with the page info as a regular Map are much more clear about what that object is

const parentSegments: string[] = []

const stack: LoaderTree[] = [tree]
while (stack.length > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the stack is unnecessary, it only ever has one element in it. Just track this with a variablelike

let currentLoaderTree = tree
while(currentLoaderTree) {
  ...
  currentLoaderTree = parallelRoutes.children
}

@@ -1393,6 +1440,44 @@ export async function buildAppStaticPaths({
)
}

function resolveAppConfig(generateParams: GenerateParams): AppConfig {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is only called in one place, consider just constructing the AppConfig while you are producing the GenerateParams. No reason to re-traverse each layer when you are already doing it

@wyattjoh wyattjoh closed this Dec 9, 2023
This was referenced Dec 9, 2023
wyattjoh added a commit that referenced this pull request Dec 11, 2023
This updates the `collectGenerateParams` method to use a loop rather
than being recursive as well as updating some of the Typescript types.
This is a follow up of #59420.

Closes NEXT-1839
wyattjoh added a commit that referenced this pull request Dec 12, 2023
This updates the some of the logic around updating `PageInfo` entries in
the `pageInfos` map. This is a followup to #59420.

Closes NEXT-1838
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Dec 23, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants