Skip to content

Commit c5a8e09

Browse files
timneutkensijjk
andauthoredAug 8, 2023
Consolidate Server and Routing process into one process (#53523)
In the current version of Next.js there are 4 processes when running in production: - Server - Routing - Rendering Pages Router - Rendering App Router This setup was introduced in order to allow App Router and Pages Router to use different versions of React (i.e. Server Actions currently requires react@experimental to function). I wrote down more on why these processes exist in this comment: #49929 (comment) This PR combines the Server and Routing process into one handler, as the "Server" process was only proxying to the Routing process. In my testing this caused about ~2x the amount of memory per request as the response body was duplicated between the processes. This was especially visible in the case of that memory leak in Node.js 18.16 as it grew memory usage on both sides quickly. In the process of going through these changes I found a couple of other bugs like the propagation of values to the worker processes not being awaited ([link](https://github.com/vercel/next.js/pull/53523/files#diff-0ef09f360141930bb03263b378d37d71ad9432ac851679aeabc577923536df84R54)) and the dot syntax for propagation was not functioning. It also seemed there were a few cases where watchpack was used that would cause many more files to be watched than expected, for now I've removed those cases, specifically the "delete dir while running" and instrument.js hmr (instrument.js is experimental). Those tests have been skipped for now until we can revisit them to verfiy it I've also cleaned up the types a bit while I was looking into these changes. ### Improvement ⚠️ Important preface to this, measuring memory usage / peak usage is not super reliable especially when JS gets garbage collected. These numbers are just to show the rough percentage of less memory usage. #### Baseline Old: ``` next-server: 44.8MB next-router-worker: 57.5MB next-render-worker-app: 39,6MB next-render-worker-pages: 39,1MB ``` New: ``` next-server: Removed next-router-worker: 64.4MB next-render-worker-app: 43.1MB (Note: no changes here, this shows what I meant by rough numbers) next-render-worker-pages: 42.4MB (Note: no changes here, this shows what I meant by rough numbers) ``` Overall win: ~40MB (process is removed) #### Peak usage Old: ``` next-server: 118.6MB next-router-worker: 223.7MB next-render-worker-app: 32.8MB (I ran the test on a pages application in this case) next-render-worker-pages: 101.1MB ``` New: ``` next-server: Removed next-router-worker: 179.1MB next-render-worker-app: 33.4MB next-render-worker-pages: 117.5MB ``` Overall win: ~100MB (but it scales with requests so it was about ~50% of next-router-worker constantly) Related: #53523 --------- Co-authored-by: JJ Kasper <jj@jjsweb.site>
1 parent fef6f82 commit c5a8e09

File tree

21 files changed

+452
-972
lines changed

21 files changed

+452
-972
lines changed
 

‎packages/next/src/cli/next-dev.ts

+140-285
Large diffs are not rendered by default.

‎packages/next/src/cli/next-start.ts

-13
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ import { startServer } from '../server/lib/start-server'
55
import { getPort, printAndExit } from '../server/lib/utils'
66
import { getProjectDir } from '../lib/get-project-dir'
77
import { CliCommand } from '../lib/commands'
8-
import { resolve } from 'path'
9-
import { PHASE_PRODUCTION_SERVER } from '../shared/lib/constants'
10-
import loadConfig from '../server/config'
118
import { getValidatedArgs } from '../lib/get-validated-args'
129

1310
const nextStart: CliCommand = async (argv) => {
@@ -66,22 +63,12 @@ const nextStart: CliCommand = async (argv) => {
6663
? Math.ceil(keepAliveTimeoutArg)
6764
: undefined
6865

69-
const config = await loadConfig(
70-
PHASE_PRODUCTION_SERVER,
71-
resolve(dir || '.'),
72-
undefined,
73-
undefined,
74-
true
75-
)
76-
7766
await startServer({
7867
dir,
79-
nextConfig: config,
8068
isDev: false,
8169
hostname: host,
8270
port,
8371
keepAliveTimeout,
84-
useWorkers: !!config.experimental.appDir,
8572
})
8673
}
8774

‎packages/next/src/server/base-server.ts

+4
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
468468
this.responseCache = this.getResponseCache({ dev })
469469
}
470470

471+
protected reloadMatchers() {
472+
return this.matchers.reload()
473+
}
474+
471475
protected async normalizeNextData(
472476
_req: BaseNextRequest,
473477
_res: BaseNextResponse,

‎packages/next/src/server/dev/next-dev-server.ts

-10
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ export interface Options extends ServerOptions {
8787
export default class DevServer extends Server {
8888
private devReady: Promise<void>
8989
private setDevReady?: Function
90-
private webpackWatcher?: any | null
9190
protected sortedRoutes?: string[]
9291
private pagesDir?: string
9392
private appDir?: string
@@ -247,15 +246,6 @@ export default class DevServer extends Server {
247246
return 'development'
248247
}
249248

250-
async stopWatcher(): Promise<void> {
251-
if (!this.webpackWatcher) {
252-
return
253-
}
254-
255-
this.webpackWatcher.close()
256-
this.webpackWatcher = null
257-
}
258-
259249
protected async prepareImpl(): Promise<void> {
260250
setGlobal('distDir', this.distDir)
261251
setGlobal('phase', PHASE_DEVELOPMENT_SERVER)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function checkIsNodeDebugging() {
2+
let isNodeDebugging: 'brk' | boolean = !!(
3+
process.execArgv.some((localArg) => localArg.startsWith('--inspect')) ||
4+
process.env.NODE_OPTIONS?.match?.(/--inspect(=\S+)?( |$)/)
5+
)
6+
7+
if (
8+
process.execArgv.some((localArg) => localArg.startsWith('--inspect-brk')) ||
9+
process.env.NODE_OPTIONS?.match?.(/--inspect-brk(=\S+)?( |$)/)
10+
) {
11+
isNodeDebugging = 'brk'
12+
}
13+
return isNodeDebugging
14+
}

‎packages/next/src/server/lib/render-server.ts

+6-13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { RequestHandler } from '../next'
33
// this must come first as it includes require hooks
44
import { initializeServerWorker } from './setup-server-worker'
55
import next from '../next'
6+
import { PropagateToWorkersField } from './router-utils/types'
67

78
export const WORKER_SELF_EXIT_CODE = 77
89

@@ -39,26 +40,18 @@ export function deleteCache(filePaths: string[]) {
3940
}
4041
}
4142

42-
export async function propagateServerField(field: string, value: any) {
43+
export async function propagateServerField(
44+
field: PropagateToWorkersField,
45+
value: any
46+
) {
4347
if (!app) {
4448
throw new Error('Invariant cant propagate server field, no app initialized')
4549
}
4650
let appField = (app as any).server
4751

48-
if (field.includes('.')) {
49-
const parts = field.split('.')
50-
51-
for (let i = 0; i < parts.length - 1; i++) {
52-
if (appField) {
53-
appField = appField[parts[i]]
54-
}
55-
}
56-
field = parts[parts.length - 1]
57-
}
58-
5952
if (appField) {
6053
if (typeof appField[field] === 'function') {
61-
appField[field].apply(
54+
await appField[field].apply(
6255
(app as any).server,
6356
Array.isArray(value) ? value : []
6457
)

‎packages/next/src/server/lib/router-server.ts

+68-58
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { IncomingMessage } from 'http'
22

33
// this must come first as it includes require hooks
4-
import { initializeServerWorker } from './setup-server-worker'
4+
import type {
5+
WorkerRequestHandler,
6+
WorkerUpgradeHandler,
7+
} from './setup-server-worker'
58

69
import url from 'url'
710
import path from 'path'
@@ -24,6 +27,7 @@ import { getResolveRoutes } from './router-utils/resolve-routes'
2427
import { NextUrlWithParsedQuery, getRequestMeta } from '../request-meta'
2528
import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix'
2629
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
30+
import setupCompression from 'next/dist/compiled/compression'
2731

2832
import {
2933
PHASE_PRODUCTION_SERVER,
@@ -32,13 +36,6 @@ import {
3236
} from '../../shared/lib/constants'
3337
import { signalFromNodeResponse } from '../web/spec-extension/adapters/next-request'
3438

35-
let initializeResult:
36-
| undefined
37-
| {
38-
port: number
39-
hostname: string
40-
}
41-
4239
const debug = setupDebug('next:router-server:main')
4340

4441
export type RenderWorker = InstanceType<
@@ -51,6 +48,13 @@ export type RenderWorker = InstanceType<
5148
propagateServerField: typeof import('./render-server').propagateServerField
5249
}
5350

51+
const nextDeleteCacheCallbacks: Array<(filePaths: string[]) => Promise<any>> =
52+
[]
53+
const nextDeleteAppClientCacheCallbacks: Array<() => Promise<any>> = []
54+
const nextClearModuleContextCallbacks: Array<
55+
(targetPath: string) => Promise<any>
56+
> = []
57+
5458
export async function initialize(opts: {
5559
dir: string
5660
port: number
@@ -61,10 +65,7 @@ export async function initialize(opts: {
6165
isNodeDebugging: boolean
6266
keepAliveTimeout?: number
6367
customServer?: boolean
64-
}): Promise<NonNullable<typeof initializeResult>> {
65-
if (initializeResult) {
66-
return initializeResult
67-
}
68+
}): Promise<[WorkerRequestHandler, WorkerUpgradeHandler]> {
6869
process.title = 'next-router-worker'
6970

7071
if (!process.env.NODE_ENV) {
@@ -80,6 +81,12 @@ export async function initialize(opts: {
8081
true
8182
)
8283

84+
let compress: ReturnType<typeof setupCompression> | undefined
85+
86+
if (config?.compress !== false) {
87+
compress = setupCompression()
88+
}
89+
8390
const fsChecker = await setupFsCheck({
8491
dev: opts.dev,
8592
dir: opts.dir,
@@ -207,39 +214,57 @@ export async function initialize(opts: {
207214
)
208215

209216
// pre-initialize workers
210-
await renderWorkers.app?.initialize(renderWorkerOpts)
211-
await renderWorkers.pages?.initialize(renderWorkerOpts)
217+
const initialized = {
218+
app: await renderWorkers.app?.initialize(renderWorkerOpts),
219+
pages: await renderWorkers.pages?.initialize(renderWorkerOpts),
220+
}
212221

213222
if (devInstance) {
214223
Object.assign(devInstance.renderWorkers, renderWorkers)
224+
225+
nextDeleteCacheCallbacks.push((filePaths: string[]) =>
226+
Promise.all([
227+
renderWorkers.pages?.deleteCache(filePaths),
228+
renderWorkers.app?.deleteCache(filePaths),
229+
])
230+
)
231+
nextDeleteAppClientCacheCallbacks.push(() =>
232+
Promise.all([
233+
renderWorkers.pages?.deleteAppClientCache(),
234+
renderWorkers.app?.deleteAppClientCache(),
235+
])
236+
)
237+
nextClearModuleContextCallbacks.push((targetPath: string) =>
238+
Promise.all([
239+
renderWorkers.pages?.clearModuleContext(targetPath),
240+
renderWorkers.app?.clearModuleContext(targetPath),
241+
])
242+
)
215243
;(global as any)._nextDeleteCache = async (filePaths: string[]) => {
216-
try {
217-
await Promise.all([
218-
renderWorkers.pages?.deleteCache(filePaths),
219-
renderWorkers.app?.deleteCache(filePaths),
220-
])
221-
} catch (err) {
222-
console.error(err)
244+
for (const cb of nextDeleteCacheCallbacks) {
245+
try {
246+
await cb(filePaths)
247+
} catch (err) {
248+
console.error(err)
249+
}
223250
}
224251
}
225252
;(global as any)._nextDeleteAppClientCache = async () => {
226-
try {
227-
await Promise.all([
228-
renderWorkers.pages?.deleteAppClientCache(),
229-
renderWorkers.app?.deleteAppClientCache(),
230-
])
231-
} catch (err) {
232-
console.error(err)
253+
for (const cb of nextDeleteAppClientCacheCallbacks) {
254+
try {
255+
await cb()
256+
} catch (err) {
257+
console.error(err)
258+
}
233259
}
234260
}
235261
;(global as any)._nextClearModuleContext = async (targetPath: string) => {
236-
try {
237-
await Promise.all([
238-
renderWorkers.pages?.clearModuleContext(targetPath),
239-
renderWorkers.app?.clearModuleContext(targetPath),
240-
])
241-
} catch (err) {
242-
console.error(err)
262+
for (const cb of nextClearModuleContextCallbacks) {
263+
try {
264+
await cb(targetPath)
265+
} catch (err) {
266+
console.error(err)
267+
}
243268
}
244269
}
245270
}
@@ -274,10 +299,11 @@ export async function initialize(opts: {
274299
devInstance?.ensureMiddleware
275300
)
276301

277-
const requestHandler: Parameters<typeof initializeServerWorker>[0] = async (
278-
req,
279-
res
280-
) => {
302+
const requestHandler: WorkerRequestHandler = async (req, res) => {
303+
if (compress) {
304+
// @ts-expect-error not express req/res
305+
compress(req, res, () => {})
306+
}
281307
req.on('error', (_err) => {
282308
// TODO: log socket errors?
283309
})
@@ -319,8 +345,7 @@ export async function initialize(opts: {
319345
return null
320346
}
321347

322-
const curWorker = renderWorkers[type]
323-
const workerResult = await curWorker?.initialize(renderWorkerOpts)
348+
const workerResult = initialized[type]
324349

325350
if (!workerResult) {
326351
throw new Error(`Failed to initialize render worker ${type}`)
@@ -690,11 +715,7 @@ export async function initialize(opts: {
690715
}
691716
}
692717

693-
const upgradeHandler: Parameters<typeof initializeServerWorker>[1] = async (
694-
req,
695-
socket,
696-
head
697-
) => {
718+
const upgradeHandler: WorkerUpgradeHandler = async (req, socket, head) => {
698719
try {
699720
req.on('error', (_err) => {
700721
// TODO: log socket errors?
@@ -735,16 +756,5 @@ export async function initialize(opts: {
735756
}
736757
}
737758

738-
const { port, hostname } = await initializeServerWorker(
739-
requestHandler,
740-
upgradeHandler,
741-
opts
742-
)
743-
744-
initializeResult = {
745-
port,
746-
hostname: hostname === '0.0.0.0' ? '127.0.0.1' : hostname,
747-
}
748-
749-
return initializeResult
759+
return [requestHandler, upgradeHandler]
750760
}

‎packages/next/src/server/lib/router-utils/setup-dev.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import findUp from 'next/dist/compiled/find-up'
1717
import { buildCustomRoute } from './filesystem'
1818
import * as Log from '../../../build/output/log'
1919
import HotReloader, { matchNextPageBundleRequest } from '../../dev/hot-reloader'
20-
import { traceGlobals } from '../../../trace/shared'
20+
import { setGlobal } from '../../../trace/shared'
2121
import { Telemetry } from '../../../telemetry/storage'
2222
import { IncomingMessage, ServerResponse } from 'http'
2323
import loadJsConfig from '../../../build/load-jsconfig'
@@ -79,6 +79,7 @@ import { PagesManifest } from '../../../build/webpack/plugins/pages-manifest-plu
7979
import { AppBuildManifest } from '../../../build/webpack/plugins/app-build-manifest-plugin'
8080
import { PageNotFoundError } from '../../../shared/lib/utils'
8181
import { srcEmptySsgManifest } from '../../../build/webpack/plugins/build-manifest-plugin'
82+
import { PropagateToWorkersField } from './types'
8283
import { MiddlewareManifest } from '../../../build/webpack/plugins/middleware-plugin'
8384

8485
type SetupOpts = {
@@ -120,8 +121,8 @@ async function startWatcher(opts: SetupOpts) {
120121

121122
const distDir = path.join(opts.dir, opts.nextConfig.distDir)
122123

123-
traceGlobals.set('distDir', distDir)
124-
traceGlobals.set('phase', PHASE_DEVELOPMENT_SERVER)
124+
setGlobal('distDir', distDir)
125+
setGlobal('phase', PHASE_DEVELOPMENT_SERVER)
125126

126127
const validFileMatcher = createValidFileMatcher(
127128
nextConfig.pageExtensions,
@@ -133,7 +134,7 @@ async function startWatcher(opts: SetupOpts) {
133134
pages?: import('../router-server').RenderWorker
134135
} = {}
135136

136-
async function propagateToWorkers(field: string, args: any) {
137+
async function propagateToWorkers(field: PropagateToWorkersField, args: any) {
137138
await renderWorkers.app?.propagateServerField(field, args)
138139
await renderWorkers.pages?.propagateServerField(field, args)
139140
}
@@ -1010,7 +1011,7 @@ async function startWatcher(opts: SetupOpts) {
10101011
hotReloader.setHmrServerError(new Error(errorMessage))
10111012
} else if (numConflicting === 0) {
10121013
hotReloader.clearHmrServerError()
1013-
await propagateToWorkers('matchers.reload', undefined)
1014+
await propagateToWorkers('reloadMatchers', undefined)
10141015
}
10151016
}
10161017

@@ -1279,7 +1280,7 @@ async function startWatcher(opts: SetupOpts) {
12791280
} finally {
12801281
// Reload the matchers. The filesystem would have been written to,
12811282
// and the matchers need to re-scan it to update the router.
1282-
await propagateToWorkers('middleware.reload', undefined)
1283+
await propagateToWorkers('reloadMatchers', undefined)
12831284
}
12841285
})
12851286

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type PropagateToWorkersField =
2+
| 'actualMiddlewareFile'
3+
| 'actualInstrumentationHookFile'
4+
| 'reloadMatchers'
5+
| 'loadEnvConfig'
6+
| 'appPathRoutes'
7+
| 'middleware'

0 commit comments

Comments
 (0)
Please sign in to comment.