Skip to content

Commit 3bf4ae3

Browse files
authoredMay 17, 2021
performance improvement of static generation (#25035)
### move all access to built pages into worker pool to allow parallelizing and avoid loading the bundles in the main thread This improves performance of the static check step a bit and helps reducing memory load in main thread ### enable splitChunks for server build in webpack 5 This improves performance for static generation by loading less code due to reduced duplication
1 parent a442053 commit 3bf4ae3

File tree

5 files changed

+205
-189
lines changed

5 files changed

+205
-189
lines changed
 

‎packages/next/build/entries.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export type WebpackEntrypoints = {
5858
| string[]
5959
| {
6060
import: string | string[]
61-
dependOn: string | string[]
61+
dependOn?: string | string[]
6262
}
6363
}
6464

‎packages/next/build/index.ts

+190-184
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,6 @@ import {
8181
detectConflictingPaths,
8282
computeFromManifest,
8383
getJsPageSizeInKb,
84-
getNamedExports,
85-
hasCustomGetInitialProps,
86-
isPageStatic,
8784
PageInfo,
8885
printCustomRoutes,
8986
printTreeView,
@@ -266,7 +263,7 @@ export default async function build(
266263
)
267264
const pageKeys = Object.keys(mappedPages)
268265
const conflictingPublicFiles: string[] = []
269-
const hasCustomErrorPage = mappedPages['/_error'].startsWith(
266+
const hasCustomErrorPage: boolean = mappedPages['/_error'].startsWith(
270267
'private-next-pages'
271268
)
272269
const hasPages404 = Boolean(
@@ -656,219 +653,228 @@ export default async function build(
656653
await promises.readFile(buildManifestPath, 'utf8')
657654
) as BuildManifest
658655

659-
let customAppGetInitialProps: boolean | undefined
660-
let namedExports: Array<string> | undefined
661-
let isNextImageImported: boolean | undefined
662656
const analysisBegin = process.hrtime()
663-
let hasSsrAmpPages = false
664657

665658
const staticCheckSpan = nextBuildSpan.traceChild('static-check')
666-
const { hasNonStaticErrorPage } = await staticCheckSpan.traceAsyncFn(
667-
async () => {
668-
process.env.NEXT_PHASE = PHASE_PRODUCTION_BUILD
659+
const {
660+
customAppGetInitialProps,
661+
namedExports,
662+
isNextImageImported,
663+
hasSsrAmpPages,
664+
hasNonStaticErrorPage,
665+
} = await staticCheckSpan.traceAsyncFn(async () => {
666+
process.env.NEXT_PHASE = PHASE_PRODUCTION_BUILD
667+
668+
const staticCheckWorkers = new Worker(staticCheckWorker, {
669+
numWorkers: config.experimental.cpus,
670+
enableWorkerThreads: config.experimental.workerThreads,
671+
}) as Worker & typeof import('./utils')
672+
673+
staticCheckWorkers.getStdout().pipe(process.stdout)
674+
staticCheckWorkers.getStderr().pipe(process.stderr)
675+
676+
const runtimeEnvConfig = {
677+
publicRuntimeConfig: config.publicRuntimeConfig,
678+
serverRuntimeConfig: config.serverRuntimeConfig,
679+
}
669680

670-
const staticCheckWorkers = new Worker(staticCheckWorker, {
671-
numWorkers: config.experimental.cpus,
672-
enableWorkerThreads: config.experimental.workerThreads,
673-
}) as Worker & { isPageStatic: typeof isPageStatic }
681+
const nonStaticErrorPageSpan = staticCheckSpan.traceChild(
682+
'check-static-error-page'
683+
)
684+
const nonStaticErrorPagePromise = nonStaticErrorPageSpan.traceAsyncFn(
685+
async () =>
686+
hasCustomErrorPage &&
687+
(await staticCheckWorkers.hasCustomGetInitialProps(
688+
'/_error',
689+
distDir,
690+
isLikeServerless,
691+
runtimeEnvConfig,
692+
false
693+
))
694+
)
695+
// we don't output _app in serverless mode so use _app export
696+
// from _error instead
697+
const appPageToCheck = isLikeServerless ? '/_error' : '/_app'
674698

675-
staticCheckWorkers.getStdout().pipe(process.stdout)
676-
staticCheckWorkers.getStderr().pipe(process.stderr)
699+
const customAppGetInitialPropsPromise = staticCheckWorkers.hasCustomGetInitialProps(
700+
appPageToCheck,
701+
distDir,
702+
isLikeServerless,
703+
runtimeEnvConfig,
704+
true
705+
)
677706

678-
const runtimeEnvConfig = {
679-
publicRuntimeConfig: config.publicRuntimeConfig,
680-
serverRuntimeConfig: config.serverRuntimeConfig,
681-
}
707+
const namedExportsPromise = staticCheckWorkers.getNamedExports(
708+
appPageToCheck,
709+
distDir,
710+
isLikeServerless,
711+
runtimeEnvConfig
712+
)
682713

683-
const nonStaticErrorPageSpan = staticCheckSpan.traceChild(
684-
'check-static-error-page'
685-
)
686-
const nonStaticErrorPage = await nonStaticErrorPageSpan.traceAsyncFn(
687-
async () =>
688-
hasCustomErrorPage &&
689-
(await hasCustomGetInitialProps(
690-
'/_error',
714+
// eslint-disable-next-line no-shadow
715+
let isNextImageImported: boolean | undefined
716+
// eslint-disable-next-line no-shadow
717+
let hasSsrAmpPages = false
718+
719+
const computedManifestData = await computeFromManifest(
720+
buildManifest,
721+
distDir,
722+
config.experimental.gzipSize
723+
)
724+
await Promise.all(
725+
pageKeys.map(async (page) => {
726+
const checkPageSpan = staticCheckSpan.traceChild('check-page', {
727+
page,
728+
})
729+
return checkPageSpan.traceAsyncFn(async () => {
730+
const actualPage = normalizePagePath(page)
731+
const [selfSize, allSize] = await getJsPageSizeInKb(
732+
actualPage,
691733
distDir,
692-
isLikeServerless,
693-
runtimeEnvConfig,
694-
false
695-
))
696-
)
697-
// we don't output _app in serverless mode so use _app export
698-
// from _error instead
699-
const appPageToCheck = isLikeServerless ? '/_error' : '/_app'
734+
buildManifest,
735+
config.experimental.gzipSize,
736+
computedManifestData
737+
)
700738

701-
customAppGetInitialProps = await hasCustomGetInitialProps(
702-
appPageToCheck,
703-
distDir,
704-
isLikeServerless,
705-
runtimeEnvConfig,
706-
true
707-
)
739+
let isSsg = false
740+
let isStatic = false
741+
let isHybridAmp = false
742+
let ssgPageRoutes: string[] | null = null
708743

709-
namedExports = await getNamedExports(
710-
appPageToCheck,
711-
distDir,
712-
isLikeServerless,
713-
runtimeEnvConfig
714-
)
744+
const nonReservedPage = !page.match(
745+
/^\/(_app|_error|_document|api(\/|$))/
746+
)
715747

716-
if (customAppGetInitialProps) {
717-
console.warn(
718-
chalk.bold.yellow(`Warning: `) +
719-
chalk.yellow(
720-
`You have opted-out of Automatic Static Optimization due to \`getInitialProps\` in \`pages/_app\`. This does not opt-out pages with \`getStaticProps\``
721-
)
722-
)
723-
console.warn(
724-
'Read more: https://nextjs.org/docs/messages/opt-out-auto-static-optimization\n'
725-
)
726-
}
748+
if (nonReservedPage) {
749+
try {
750+
let isPageStaticSpan = checkPageSpan.traceChild(
751+
'is-page-static'
752+
)
753+
let workerResult = await isPageStaticSpan.traceAsyncFn(() => {
754+
return staticCheckWorkers.isPageStatic(
755+
page,
756+
distDir,
757+
isLikeServerless,
758+
runtimeEnvConfig,
759+
config.i18n?.locales,
760+
config.i18n?.defaultLocale,
761+
isPageStaticSpan.id
762+
)
763+
})
727764

728-
const computedManifestData = await computeFromManifest(
729-
buildManifest,
730-
distDir,
731-
config.experimental.gzipSize
732-
)
733-
await Promise.all(
734-
pageKeys.map(async (page) => {
735-
const checkPageSpan = staticCheckSpan.traceChild('check-page', {
736-
page,
737-
})
738-
return checkPageSpan.traceAsyncFn(async () => {
739-
const actualPage = normalizePagePath(page)
740-
const [selfSize, allSize] = await getJsPageSizeInKb(
741-
actualPage,
742-
distDir,
743-
buildManifest,
744-
config.experimental.gzipSize,
745-
computedManifestData
746-
)
765+
if (
766+
workerResult.isStatic === false &&
767+
(workerResult.isHybridAmp || workerResult.isAmpOnly)
768+
) {
769+
hasSsrAmpPages = true
770+
}
747771

748-
let isSsg = false
749-
let isStatic = false
750-
let isHybridAmp = false
751-
let ssgPageRoutes: string[] | null = null
772+
if (workerResult.isHybridAmp) {
773+
isHybridAmp = true
774+
hybridAmpPages.add(page)
775+
}
752776

753-
const nonReservedPage = !page.match(
754-
/^\/(_app|_error|_document|api(\/|$))/
755-
)
777+
if (workerResult.isNextImageImported) {
778+
isNextImageImported = true
779+
}
756780

757-
if (nonReservedPage) {
758-
try {
759-
let isPageStaticSpan = checkPageSpan.traceChild(
760-
'is-page-static'
761-
)
762-
let workerResult = await isPageStaticSpan.traceAsyncFn(() => {
763-
return staticCheckWorkers.isPageStatic(
764-
page,
765-
distDir,
766-
isLikeServerless,
767-
runtimeEnvConfig,
768-
config.i18n?.locales,
769-
config.i18n?.defaultLocale,
770-
isPageStaticSpan.id
771-
)
772-
})
781+
if (workerResult.hasStaticProps) {
782+
ssgPages.add(page)
783+
isSsg = true
773784

774785
if (
775-
workerResult.isStatic === false &&
776-
(workerResult.isHybridAmp || workerResult.isAmpOnly)
786+
workerResult.prerenderRoutes &&
787+
workerResult.encodedPrerenderRoutes
777788
) {
778-
hasSsrAmpPages = true
779-
}
780-
781-
if (workerResult.isHybridAmp) {
782-
isHybridAmp = true
783-
hybridAmpPages.add(page)
784-
}
785-
786-
if (workerResult.isNextImageImported) {
787-
isNextImageImported = true
788-
}
789-
790-
if (workerResult.hasStaticProps) {
791-
ssgPages.add(page)
792-
isSsg = true
793-
794-
if (
795-
workerResult.prerenderRoutes &&
789+
additionalSsgPaths.set(page, workerResult.prerenderRoutes)
790+
additionalSsgPathsEncoded.set(
791+
page,
796792
workerResult.encodedPrerenderRoutes
797-
) {
798-
additionalSsgPaths.set(page, workerResult.prerenderRoutes)
799-
additionalSsgPathsEncoded.set(
800-
page,
801-
workerResult.encodedPrerenderRoutes
802-
)
803-
ssgPageRoutes = workerResult.prerenderRoutes
804-
}
805-
806-
if (workerResult.prerenderFallback === 'blocking') {
807-
ssgBlockingFallbackPages.add(page)
808-
} else if (workerResult.prerenderFallback === true) {
809-
ssgStaticFallbackPages.add(page)
810-
}
811-
} else if (workerResult.hasServerProps) {
812-
serverPropsPages.add(page)
813-
} else if (
814-
workerResult.isStatic &&
815-
customAppGetInitialProps === false
816-
) {
817-
staticPages.add(page)
818-
isStatic = true
793+
)
794+
ssgPageRoutes = workerResult.prerenderRoutes
819795
}
820796

821-
if (hasPages404 && page === '/404') {
822-
if (
823-
!workerResult.isStatic &&
824-
!workerResult.hasStaticProps
825-
) {
826-
throw new Error(
827-
`\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
828-
)
829-
}
830-
// we need to ensure the 404 lambda is present since we use
831-
// it when _app has getInitialProps
832-
if (
833-
customAppGetInitialProps &&
834-
!workerResult.hasStaticProps
835-
) {
836-
staticPages.delete(page)
837-
}
797+
if (workerResult.prerenderFallback === 'blocking') {
798+
ssgBlockingFallbackPages.add(page)
799+
} else if (workerResult.prerenderFallback === true) {
800+
ssgStaticFallbackPages.add(page)
838801
}
802+
} else if (workerResult.hasServerProps) {
803+
serverPropsPages.add(page)
804+
} else if (
805+
workerResult.isStatic &&
806+
(await customAppGetInitialPropsPromise) === false
807+
) {
808+
staticPages.add(page)
809+
isStatic = true
810+
}
839811

812+
if (hasPages404 && page === '/404') {
813+
if (!workerResult.isStatic && !workerResult.hasStaticProps) {
814+
throw new Error(
815+
`\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
816+
)
817+
}
818+
// we need to ensure the 404 lambda is present since we use
819+
// it when _app has getInitialProps
840820
if (
841-
STATIC_STATUS_PAGES.includes(page) &&
842-
!workerResult.isStatic &&
821+
(await customAppGetInitialPropsPromise) &&
843822
!workerResult.hasStaticProps
844823
) {
845-
throw new Error(
846-
`\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
847-
)
824+
staticPages.delete(page)
848825
}
849-
} catch (err) {
850-
if (err.message !== 'INVALID_DEFAULT_EXPORT') throw err
851-
invalidPages.add(page)
852826
}
827+
828+
if (
829+
STATIC_STATUS_PAGES.includes(page) &&
830+
!workerResult.isStatic &&
831+
!workerResult.hasStaticProps
832+
) {
833+
throw new Error(
834+
`\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
835+
)
836+
}
837+
} catch (err) {
838+
if (err.message !== 'INVALID_DEFAULT_EXPORT') throw err
839+
invalidPages.add(page)
853840
}
841+
}
854842

855-
pageInfos.set(page, {
856-
size: selfSize,
857-
totalSize: allSize,
858-
static: isStatic,
859-
isSsg,
860-
isHybridAmp,
861-
ssgPageRoutes,
862-
initialRevalidateSeconds: false,
863-
})
843+
pageInfos.set(page, {
844+
size: selfSize,
845+
totalSize: allSize,
846+
static: isStatic,
847+
isSsg,
848+
isHybridAmp,
849+
ssgPageRoutes,
850+
initialRevalidateSeconds: false,
864851
})
865852
})
866-
)
867-
staticCheckWorkers.end()
868-
869-
return { hasNonStaticErrorPage: nonStaticErrorPage }
853+
})
854+
)
855+
const returnValue = {
856+
customAppGetInitialProps: await customAppGetInitialPropsPromise,
857+
namedExports: await namedExportsPromise,
858+
isNextImageImported,
859+
hasSsrAmpPages,
860+
hasNonStaticErrorPage: await nonStaticErrorPagePromise,
870861
}
871-
)
862+
863+
staticCheckWorkers.end()
864+
return returnValue
865+
})
866+
867+
if (customAppGetInitialProps) {
868+
console.warn(
869+
chalk.bold.yellow(`Warning: `) +
870+
chalk.yellow(
871+
`You have opted-out of Automatic Static Optimization due to \`getInitialProps\` in \`pages/_app\`. This does not opt-out pages with \`getStaticProps\``
872+
)
873+
)
874+
console.warn(
875+
'Read more: https://nextjs.org/docs/messages/opt-out-auto-static-optimization\n'
876+
)
877+
}
872878

873879
if (!hasSsrAmpPages) {
874880
requiredServerFiles.ignore.push(

‎packages/next/build/webpack-config.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,17 @@ export default async function getBaseWebpackConfig(
822822
...(isWebpack5 ? { emitOnErrors: !dev } : { noEmitOnErrors: dev }),
823823
checkWasmTypes: false,
824824
nodeEnv: false,
825-
splitChunks: isServer ? false : splitChunksConfig,
825+
splitChunks: isServer
826+
? isWebpack5
827+
? {
828+
// allow to split entrypoints
829+
chunks: 'all',
830+
// size of files is not so relevant for server build
831+
// we want to prefer deduplication to load less code
832+
minSize: 1000,
833+
}
834+
: false
835+
: splitChunksConfig,
826836
runtimeChunk: isServer
827837
? isWebpack5 && !isLikeServerless
828838
? { name: 'webpack-runtime' }

‎packages/next/build/webpack/plugins/pages-manifest-plugin.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default class PagesManifestPlugin implements webpack.Plugin {
3838
!file.includes('webpack-runtime') && file.endsWith('.js')
3939
)
4040

41-
if (files.length > 1) {
41+
if (!isWebpack5 && files.length > 1) {
4242
console.log(
4343
`Found more than one file in server entrypoint ${entrypoint.name}`,
4444
files
@@ -47,7 +47,7 @@ export default class PagesManifestPlugin implements webpack.Plugin {
4747
}
4848

4949
// Write filename, replace any backslashes in path (on windows) with forwardslashes for cross-platform consistency.
50-
pages[pagePath] = files[0]
50+
pages[pagePath] = files[files.length - 1]
5151

5252
if (isWebpack5 && !this.dev) {
5353
pages[pagePath] = pages[pagePath].slice(3)

‎packages/next/client/next-dev.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ initNext({ webpackHMR })
4848
const { pages } = JSON.parse(event.data)
4949
const router = window.next.router
5050

51-
if (pages.includes(router.pathname)) {
51+
if (!router.clc && pages.includes(router.pathname)) {
5252
console.log('Refreshing page data due to server-side change')
5353

5454
buildIndicatorHandler('building')

0 commit comments

Comments
 (0)
Please sign in to comment.