From df1eec3be694b5952e9739fd3812f54e92f45f1b Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Sun, 18 Apr 2021 12:28:09 +0200 Subject: [PATCH] startup performance improvements (#24129) * import next-server logic during the time the configuration is loaded * load minimizer plugins only when used * load ReactDevOverlay only when used * load only meta information of tsconfig for validation * make worker for configuration loading lighter * only load runTypeCheck when used * load postcss config only when used --- packages/next/build/webpack-config.ts | 48 ++++---- .../terser-webpack-plugin/src/index.js | 4 +- .../typescript/getTypeScriptConfiguration.ts | 18 ++- .../typescript/writeConfigurationDefaults.ts | 2 +- packages/next/lib/verifyTypeScriptSetup.ts | 4 +- .../next-server/server/config-utils-worker.ts | 101 ++++++++++++++++ .../next/next-server/server/config-utils.ts | 111 ++---------------- packages/next/server/next-dev-server.ts | 11 +- packages/next/server/next.ts | 23 +++- 9 files changed, 188 insertions(+), 134 deletions(-) create mode 100644 packages/next/next-server/server/config-utils-worker.ts diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index c4906392837c36..7e9737a765e57e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -4,8 +4,6 @@ import crypto from 'crypto' import { readFileSync, realpathSync } from 'fs' import chalk from 'chalk' import semver from 'next/dist/compiled/semver' -// @ts-ignore No typings yet -import TerserPlugin from './webpack/plugins/terser-webpack-plugin/src/index.js' import path from 'path' import { webpack, isWebpack5 } from 'next/dist/compiled/webpack/webpack' import { @@ -42,7 +40,6 @@ import { pluginLoaderOptions } from './webpack/loaders/next-plugin-loader' import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin' import BuildStatsPlugin from './webpack/plugins/build-stats-plugin' import ChunkNamesPlugin from './webpack/plugins/chunk-names-plugin' -import { CssMinimizerPlugin } from './webpack/plugins/css-minimizer-plugin' import { JsConfigPathsPlugin } from './webpack/plugins/jsconfig-paths-plugin' import { DropClientPage } from './webpack/plugins/next-drop-client-page-plugin' import NextJsSsrImportPlugin from './webpack/plugins/nextjs-ssr-import' @@ -332,7 +329,7 @@ export default async function getBaseWebpackConfig( // jsconfig is a subset of tsconfig if (useTypeScript) { const ts = (await import(typeScriptPath!)) as typeof import('typescript') - const tsConfig = await getTypeScriptConfiguration(ts, tsConfigPath) + const tsConfig = await getTypeScriptConfiguration(ts, tsConfigPath, true) jsConfig = { compilerOptions: tsConfig.options } } @@ -822,24 +819,35 @@ export default async function getBaseWebpackConfig( minimize: !(dev || isServer), minimizer: [ // Minify JavaScript - new TerserPlugin({ - cacheDir: path.join(distDir, 'cache', 'next-minifier'), - parallel: config.experimental.cpus, - terserOptions, - }), + (compiler: webpack.Compiler) => { + // @ts-ignore No typings yet + const { + TerserPlugin, + } = require('./webpack/plugins/terser-webpack-plugin/src/index.js') + new TerserPlugin({ + cacheDir: path.join(distDir, 'cache', 'next-minifier'), + parallel: config.experimental.cpus, + terserOptions, + }).apply(compiler) + }, // Minify CSS - new CssMinimizerPlugin({ - postcssOptions: { - map: { - // `inline: false` generates the source map in a separate file. - // Otherwise, the CSS file is needlessly large. - inline: false, - // `annotation: false` skips appending the `sourceMappingURL` - // to the end of the CSS file. Webpack already handles this. - annotation: false, + (compiler: webpack.Compiler) => { + const { + CssMinimizerPlugin, + } = require('./webpack/plugins/css-minimizer-plugin') + new CssMinimizerPlugin({ + postcssOptions: { + map: { + // `inline: false` generates the source map in a separate file. + // Otherwise, the CSS file is needlessly large. + inline: false, + // `annotation: false` skips appending the `sourceMappingURL` + // to the end of the CSS file. Webpack already handles this. + annotation: false, + }, }, - }, - }), + }).apply(compiler) + }, ], }, context: dir, diff --git a/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js b/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js index bfe6e2c5210151..1909410fd05649 100644 --- a/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js +++ b/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js @@ -102,7 +102,7 @@ class Webpack4Cache { } } -class TerserPlugin { +export class TerserPlugin { constructor(options = {}) { const { cacheDir, terserOptions = {}, parallel } = options @@ -400,5 +400,3 @@ class TerserPlugin { }) } } - -export default TerserPlugin diff --git a/packages/next/lib/typescript/getTypeScriptConfiguration.ts b/packages/next/lib/typescript/getTypeScriptConfiguration.ts index ccd13ba21fc37b..97d7dc7c77c7e3 100644 --- a/packages/next/lib/typescript/getTypeScriptConfiguration.ts +++ b/packages/next/lib/typescript/getTypeScriptConfiguration.ts @@ -5,7 +5,8 @@ import { FatalTypeScriptError } from './FatalTypeScriptError' export async function getTypeScriptConfiguration( ts: typeof import('typescript'), - tsConfigPath: string + tsConfigPath: string, + metaOnly?: boolean ): Promise { try { const formatDiagnosticsHost: import('typescript').FormatDiagnosticsHost = { @@ -21,9 +22,20 @@ export async function getTypeScriptConfiguration( ) } + let configToParse: any = config + const result = ts.parseJsonConfigFileContent( - config, - ts.sys, + configToParse, + // When only interested in meta info, + // avoid enumerating all files (for performance reasons) + metaOnly + ? { + ...ts.sys, + readDirectory(_path, extensions, _excludes, _includes, _depth) { + return [extensions ? `file${extensions[0]}` : `file.ts`] + }, + } + : ts.sys, path.dirname(tsConfigPath) ) diff --git a/packages/next/lib/typescript/writeConfigurationDefaults.ts b/packages/next/lib/typescript/writeConfigurationDefaults.ts index ffdf748e7d4b4c..61af02eca7f316 100644 --- a/packages/next/lib/typescript/writeConfigurationDefaults.ts +++ b/packages/next/lib/typescript/writeConfigurationDefaults.ts @@ -98,7 +98,7 @@ export async function writeConfigurationDefaults( const { options: tsOptions, raw: rawConfig, - } = await getTypeScriptConfiguration(ts, tsConfigPath) + } = await getTypeScriptConfiguration(ts, tsConfigPath, true) const userTsConfigContent = await fs.readFile(tsConfigPath, { encoding: 'utf8', diff --git a/packages/next/lib/verifyTypeScriptSetup.ts b/packages/next/lib/verifyTypeScriptSetup.ts index 751d52fea99413..14b28785791d28 100644 --- a/packages/next/lib/verifyTypeScriptSetup.ts +++ b/packages/next/lib/verifyTypeScriptSetup.ts @@ -6,7 +6,7 @@ import { hasNecessaryDependencies, NecessaryDependencies, } from './typescript/hasNecessaryDependencies' -import { runTypeCheck, TypeCheckResult } from './typescript/runTypeCheck' +import type { TypeCheckResult } from './typescript/runTypeCheck' import { TypeScriptCompileError } from './typescript/TypeScriptCompileError' import { writeAppTypeDeclarations } from './typescript/writeAppTypeDeclarations' import { writeConfigurationDefaults } from './typescript/writeConfigurationDefaults' @@ -41,6 +41,8 @@ export async function verifyTypeScriptSetup( await writeAppTypeDeclarations(dir) if (typeCheckPreflight) { + const { runTypeCheck } = require('./typescript/runTypeCheck') + // Verify the project passes type-checking before we go to webpack phase: return await runTypeCheck(ts, dir, tsConfigPath) } diff --git a/packages/next/next-server/server/config-utils-worker.ts b/packages/next/next-server/server/config-utils-worker.ts new file mode 100644 index 00000000000000..7897d460c2629e --- /dev/null +++ b/packages/next/next-server/server/config-utils-worker.ts @@ -0,0 +1,101 @@ +import { loadEnvConfig } from '@next/env' +import findUp from 'next/dist/compiled/find-up' +import { init as initWebpack } from 'next/dist/compiled/webpack/webpack' +import { CONFIG_FILE, PHASE_DEVELOPMENT_SERVER } from '../lib/constants' +import { NextConfig, normalizeConfig } from './config-shared' +import * as Log from '../../build/output/log' + +let installed: boolean = false + +export function install(useWebpack5: boolean) { + if (installed) { + return + } + installed = true + + initWebpack(useWebpack5) + + // hook the Node.js require so that webpack requires are + // routed to the bundled and now initialized webpack version + require('../../build/webpack/require-hook') +} + +export type CheckReasons = + | 'test-mode' + | 'no-config' + | 'future-flag' + | 'no-future-flag' + | 'no-webpack-config' + | 'webpack-config' + +export type CheckResult = { + enabled: boolean + reason: CheckReasons +} + +export async function shouldLoadWithWebpack5( + phase: string, + dir: string +): Promise { + await loadEnvConfig(dir, phase === PHASE_DEVELOPMENT_SERVER, Log) + + const path = await findUp(CONFIG_FILE, { + cwd: dir, + }) + + if (Number(process.env.NEXT_PRIVATE_TEST_WEBPACK5_MODE) > 0) { + return { + enabled: true, + reason: 'test-mode', + } + } + + // No `next.config.js`: + if (!path?.length) { + // Uncomment to add auto-enable when there is no next.config.js + // Use webpack 5 by default in new apps: + return { + enabled: true, + reason: 'no-config', + } + } + + // Default to webpack 4 for backwards compatibility on boot: + install(false) + + const userConfigModule = require(path) + const userConfig: Partial = normalizeConfig( + phase, + userConfigModule.default || userConfigModule + ) + + // Opted-in manually + if (userConfig.future?.webpack5 === true) { + return { + enabled: true, + reason: 'future-flag', + } + } + + // Opted-out manually + if (userConfig.future?.webpack5 === false) { + return { + enabled: false, + reason: 'no-future-flag', + } + } + + // Uncomment to add auto-enable when there is no custom webpack config + // The user isn't configuring webpack + if (!userConfig.webpack) { + return { + enabled: true, + reason: 'no-webpack-config', + } + } + + return { + enabled: false, + reason: 'webpack-config', + } +} diff --git a/packages/next/next-server/server/config-utils.ts b/packages/next/next-server/server/config-utils.ts index 373c81199a1722..742b30bca485f1 100644 --- a/packages/next/next-server/server/config-utils.ts +++ b/packages/next/next-server/server/config-utils.ts @@ -1,105 +1,10 @@ -import { loadEnvConfig } from '@next/env' +import path from 'path' import { Worker } from 'jest-worker' -import findUp from 'next/dist/compiled/find-up' -import { init as initWebpack } from 'next/dist/compiled/webpack/webpack' -import { CONFIG_FILE, PHASE_DEVELOPMENT_SERVER } from '../lib/constants' -import { NextConfig, normalizeConfig } from './config-shared' import * as Log from '../../build/output/log' +import type { CheckReasons, CheckResult } from './config-utils-worker' +import { install, shouldLoadWithWebpack5 } from './config-utils-worker' -let installed: boolean = false - -export function install(useWebpack5: boolean) { - if (installed) { - return - } - installed = true - - initWebpack(useWebpack5) - - // hook the Node.js require so that webpack requires are - // routed to the bundled and now initialized webpack version - require('../../build/webpack/require-hook') -} - -type CheckReasons = - | 'test-mode' - | 'no-config' - | 'future-flag' - | 'no-future-flag' - | 'no-webpack-config' - | 'webpack-config' - -type CheckResult = { - enabled: boolean - reason: CheckReasons -} - -export async function shouldLoadWithWebpack5( - phase: string, - dir: string -): Promise { - await loadEnvConfig(dir, phase === PHASE_DEVELOPMENT_SERVER, Log) - - const path = await findUp(CONFIG_FILE, { - cwd: dir, - }) - - if (Number(process.env.NEXT_PRIVATE_TEST_WEBPACK5_MODE) > 0) { - return { - enabled: true, - reason: 'test-mode', - } - } - - // No `next.config.js`: - if (!path?.length) { - // Uncomment to add auto-enable when there is no next.config.js - // Use webpack 5 by default in new apps: - return { - enabled: true, - reason: 'no-config', - } - } - - // Default to webpack 4 for backwards compatibility on boot: - install(false) - - const userConfigModule = require(path) - const userConfig: Partial = normalizeConfig( - phase, - userConfigModule.default || userConfigModule - ) - - // Opted-in manually - if (userConfig.future?.webpack5 === true) { - return { - enabled: true, - reason: 'future-flag', - } - } - - // Opted-out manually - if (userConfig.future?.webpack5 === false) { - return { - enabled: false, - reason: 'no-future-flag', - } - } - - // Uncomment to add auto-enable when there is no custom webpack config - // The user isn't configuring webpack - if (!userConfig.webpack) { - return { - enabled: true, - reason: 'no-webpack-config', - } - } - - return { - enabled: false, - reason: 'webpack-config', - } -} +export { install, shouldLoadWithWebpack5 } function reasonMessage(reason: CheckReasons) { switch (reason) { @@ -122,7 +27,13 @@ function reasonMessage(reason: CheckReasons) { export async function loadWebpackHook(phase: string, dir: string) { let useWebpack5 = false - const worker: any = new Worker(__filename, { enableWorkerThreads: false }) + const worker: any = new Worker( + path.resolve(__dirname, './config-utils-worker.js'), + { + enableWorkerThreads: false, + numWorkers: 1, + } + ) try { const result: CheckResult = await worker.shouldLoadWithWebpack5(phase, dir) Log.info( diff --git a/packages/next/server/next-dev-server.ts b/packages/next/server/next-dev-server.ts index 0d095c62e82135..85979fecc73ab8 100644 --- a/packages/next/server/next-dev-server.ts +++ b/packages/next/server/next-dev-server.ts @@ -1,4 +1,3 @@ -import { ReactDevOverlay } from '@next/react-dev-overlay/lib/client' import crypto from 'crypto' import fs from 'fs' import { IncomingMessage, ServerResponse } from 'http' @@ -47,6 +46,16 @@ if (typeof React.Suspense === 'undefined') { ) } +// Load ReactDevOverlay only when needed +let ReactDevOverlayImpl: React.FunctionComponent +const ReactDevOverlay = (props: any) => { + if (ReactDevOverlayImpl === undefined) { + ReactDevOverlayImpl = require('@next/react-dev-overlay/lib/client') + .ReactDevOverlay + } + return ReactDevOverlayImpl(props) +} + export default class DevServer extends Server { private devReady: Promise private setDevReady?: Function diff --git a/packages/next/server/next.ts b/packages/next/server/next.ts index d834c7ad48555c..6fd8c0e17ca213 100644 --- a/packages/next/server/next.ts +++ b/packages/next/server/next.ts @@ -1,4 +1,8 @@ -import Server, { ServerConstructor } from '../next-server/server/next-server' +import '../next-server/server/node-polyfill-fetch' +import type { + default as Server, + ServerConstructor, +} from '../next-server/server/next-server' import { NON_STANDARD_NODE_ENV } from '../lib/constants' import * as log from '../build/output/log' import loadConfig, { NextConfig } from '../next-server/server/config' @@ -17,6 +21,14 @@ type NextServerConstructor = ServerConstructor & { dev?: boolean } +let ServerImpl: typeof Server + +const getServerImpl = async () => { + if (ServerImpl === undefined) + ServerImpl = (await import('../next-server/server/next-server')).default + return ServerImpl +} + export class NextServer { private serverPromise?: Promise private server?: Server @@ -93,17 +105,17 @@ export class NextServer { return (server as any).close() } - private createServer( + private async createServer( options: NextServerConstructor & { conf: NextConfig isNextDevCommand?: boolean } - ): Server { + ): Promise { if (options.dev) { const DevServer = require('./next-dev-server').default return new DevServer(options) } - return new Server(options) + return new (await getServerImpl())(options) } private async loadConfig() { @@ -117,8 +129,9 @@ export class NextServer { private async getServer() { if (!this.serverPromise) { + setTimeout(getServerImpl, 10) this.serverPromise = this.loadConfig().then(async (conf) => { - this.server = this.createServer({ + this.server = await this.createServer({ ...this.options, conf, })