Skip to content

Commit

Permalink
startup performance improvements (vercel#24129)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sokra authored and SokratisVidros committed Apr 20, 2021
1 parent 97fcc93 commit df1eec3
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 134 deletions.
48 changes: 28 additions & 20 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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 }
}

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class Webpack4Cache {
}
}

class TerserPlugin {
export class TerserPlugin {
constructor(options = {}) {
const { cacheDir, terserOptions = {}, parallel } = options

Expand Down Expand Up @@ -400,5 +400,3 @@ class TerserPlugin {
})
}
}

export default TerserPlugin
18 changes: 15 additions & 3 deletions packages/next/lib/typescript/getTypeScriptConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { FatalTypeScriptError } from './FatalTypeScriptError'

export async function getTypeScriptConfiguration(
ts: typeof import('typescript'),
tsConfigPath: string
tsConfigPath: string,
metaOnly?: boolean
): Promise<import('typescript').ParsedCommandLine> {
try {
const formatDiagnosticsHost: import('typescript').FormatDiagnosticsHost = {
Expand All @@ -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)
)

Expand Down
2 changes: 1 addition & 1 deletion packages/next/lib/typescript/writeConfigurationDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 3 additions & 1 deletion packages/next/lib/verifyTypeScriptSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}
Expand Down
101 changes: 101 additions & 0 deletions packages/next/next-server/server/config-utils-worker.ts
Original file line number Diff line number Diff line change
@@ -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<CheckResult> {
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<NextConfig> = 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',
}
}
111 changes: 11 additions & 100 deletions packages/next/next-server/server/config-utils.ts
Original file line number Diff line number Diff line change
@@ -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<CheckResult> {
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<NextConfig> = 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) {
Expand All @@ -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(
Expand Down
Loading

0 comments on commit df1eec3

Please sign in to comment.