mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
a39366938a
## Summary Development scripts for profiling and benchmarking Next.js dev server boot time. ### Benchmarking Scripts - `benchmark-boot-time.sh` - Wall-clock benchmarking with two metrics: - **Listen time**: When TCP port accepts connections - **Ready time**: When first HTTP request succeeds - `benchmark-next-dev-boot.js` - Multi-iteration benchmarking with statistics (median, p95, stddev) ### Profiling Scripts - `profile-next-dev-boot.js` - CPU profiling infrastructure using V8 inspector - `analyze-profile.js` - Analyze CPU profiles to identify hot modules by CPU time ### Analysis Scripts - `analyze-dev-server-bundle.js` - Bundle analyzer for dev server (generates treemap report) - `trace-cli-startup.js` - Module loading trace to identify slow imports ## Usage ```bash # Benchmark dev server boot time (5 runs by default) ./scripts/benchmark-boot-time.sh # Multi-iteration benchmark with stats node scripts/benchmark-next-dev-boot.js --iterations 10 # Generate CPU profile node scripts/profile-next-dev-boot.js # Analyze profile output node scripts/analyze-profile.js .next/cpu-profiles/*.cpuprofile # Analyze dev server bundle node scripts/analyze-dev-server-bundle.js --open ``` ## Test Plan - [x] Run `./scripts/benchmark-boot-time.sh` locally - [x] Verify scripts execute without errors
246 lines
7.1 KiB
JavaScript
Executable File
246 lines
7.1 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Dev Server Boot Time Benchmark
|
|
*
|
|
* Usage:
|
|
* node scripts/benchmark-boot.js [options]
|
|
*
|
|
* Options:
|
|
* --iterations=N Number of iterations (default: 5)
|
|
* --test-dir=PATH Test project directory (default: /private/tmp/next-boot-test)
|
|
* --bundled Use bundled dev server (default)
|
|
* --unbundled Use unbundled dev server
|
|
* --compare Run both bundled and unbundled for comparison
|
|
* --turbopack Use Turbopack (default)
|
|
* --webpack Use Webpack
|
|
*/
|
|
|
|
const { spawn, execSync } = require('child_process')
|
|
const path = require('path')
|
|
const fs = require('fs')
|
|
|
|
// Parse arguments
|
|
const args = process.argv.slice(2)
|
|
const getArg = (name, defaultValue) => {
|
|
const arg = args.find((a) => a.startsWith(`--${name}=`))
|
|
return arg ? arg.split('=')[1] : defaultValue
|
|
}
|
|
const hasFlag = (name) => args.includes(`--${name}`)
|
|
|
|
const iterations = parseInt(getArg('iterations', '5'), 10)
|
|
const testDir = getArg('test-dir', '/private/tmp/next-boot-test')
|
|
const compare = hasFlag('compare')
|
|
const useWebpack = hasFlag('webpack')
|
|
const bundlerFlag = useWebpack ? '--webpack' : '--turbopack'
|
|
|
|
const nextDir = path.join(__dirname, '..', 'packages', 'next')
|
|
const nextBin = path.join(nextDir, 'dist/bin/next')
|
|
const cliSource = path.join(nextDir, 'src/cli/next-dev.ts')
|
|
|
|
console.log('\x1b[34m=== Next.js Dev Server Boot Benchmark ===\x1b[0m')
|
|
console.log(`Iterations: ${iterations}`)
|
|
console.log(`Test directory: ${testDir}`)
|
|
console.log(`Bundler: ${useWebpack ? 'Webpack' : 'Turbopack'}`)
|
|
console.log('')
|
|
|
|
// Verify test directory exists
|
|
if (!fs.existsSync(testDir)) {
|
|
console.error(
|
|
`\x1b[31mError: Test directory does not exist: ${testDir}\x1b[0m`
|
|
)
|
|
console.log('Create a test project first:')
|
|
console.log(` mkdir -p ${testDir} && cd ${testDir}`)
|
|
console.log(' pnpm init && pnpm add next@canary react react-dom')
|
|
console.log(
|
|
' mkdir -p app && echo "export default function Page() { return <h1>Hello</h1> }" > app/page.tsx'
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Kill existing next dev processes
|
|
function killNextDev() {
|
|
try {
|
|
execSync('pkill -f "next dev"', { stdio: 'ignore' })
|
|
} catch {}
|
|
}
|
|
|
|
// Run a single benchmark iteration
|
|
function runIteration() {
|
|
return new Promise((resolve, reject) => {
|
|
// Clean .next directory
|
|
const nextCache = path.join(testDir, '.next')
|
|
if (fs.existsSync(nextCache)) {
|
|
fs.rmSync(nextCache, { recursive: true, force: true })
|
|
}
|
|
|
|
const startTime = Date.now()
|
|
let resolved = false
|
|
|
|
const child = spawn(nextBin, ['dev', bundlerFlag], {
|
|
cwd: testDir,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
env: { ...process.env, FORCE_COLOR: '0' },
|
|
})
|
|
|
|
let output = ''
|
|
|
|
const onData = (data) => {
|
|
output += data.toString()
|
|
// Look for "Ready in Xms" pattern
|
|
const match = output.match(/Ready in (\d+)ms/)
|
|
if (match && !resolved) {
|
|
resolved = true
|
|
const reportedTime = parseInt(match[1], 10)
|
|
const actualTime = Date.now() - startTime
|
|
child.kill('SIGTERM')
|
|
resolve({ reportedTime, actualTime })
|
|
}
|
|
}
|
|
|
|
child.stdout.on('data', onData)
|
|
child.stderr.on('data', onData)
|
|
|
|
child.on('error', (err) => {
|
|
if (!resolved) {
|
|
resolved = true
|
|
reject(err)
|
|
}
|
|
})
|
|
|
|
// Timeout after 60 seconds
|
|
setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true
|
|
child.kill('SIGKILL')
|
|
reject(new Error('Timeout waiting for server to start'))
|
|
}
|
|
}, 60000)
|
|
})
|
|
}
|
|
|
|
// Run benchmark with multiple iterations
|
|
async function runBenchmark(name) {
|
|
console.log(`\x1b[33mRunning ${name}...\x1b[0m`)
|
|
|
|
const reportedTimes = []
|
|
const actualTimes = []
|
|
|
|
for (let i = 1; i <= iterations; i++) {
|
|
try {
|
|
killNextDev()
|
|
await new Promise((r) => setTimeout(r, 500))
|
|
|
|
const { reportedTime, actualTime } = await runIteration()
|
|
reportedTimes.push(reportedTime)
|
|
actualTimes.push(actualTime)
|
|
console.log(
|
|
` Run ${i}: ${reportedTime}ms (reported) / ${actualTime}ms (actual)`
|
|
)
|
|
} catch (err) {
|
|
console.log(` Run ${i}: Failed - ${err.message}`)
|
|
}
|
|
}
|
|
|
|
killNextDev()
|
|
|
|
if (reportedTimes.length === 0) {
|
|
console.log('\x1b[31mNo successful runs\x1b[0m')
|
|
return null
|
|
}
|
|
|
|
// Calculate statistics
|
|
const calcStats = (times) => {
|
|
const sum = times.reduce((a, b) => a + b, 0)
|
|
const avg = Math.round(sum / times.length)
|
|
const min = Math.min(...times)
|
|
const max = Math.max(...times)
|
|
const sorted = [...times].sort((a, b) => a - b)
|
|
const median = sorted[Math.floor(sorted.length / 2)]
|
|
return { avg, min, max, median, count: times.length }
|
|
}
|
|
|
|
const reported = calcStats(reportedTimes)
|
|
const actual = calcStats(actualTimes)
|
|
|
|
console.log(`\x1b[32mResults for ${name}:\x1b[0m`)
|
|
console.log(` Reported time (Next.js internal):`)
|
|
console.log(
|
|
` Avg: ${reported.avg}ms | Min: ${reported.min}ms | Max: ${reported.max}ms | Median: ${reported.median}ms`
|
|
)
|
|
console.log(` Actual time (CLI to ready):`)
|
|
console.log(
|
|
` Avg: ${actual.avg}ms | Min: ${actual.min}ms | Max: ${actual.max}ms | Median: ${actual.median}ms`
|
|
)
|
|
console.log('')
|
|
|
|
return { reported, actual }
|
|
}
|
|
|
|
// Switch between bundled/unbundled
|
|
function setBundled(useBundled) {
|
|
const content = fs.readFileSync(cliSource, 'utf-8')
|
|
|
|
const bundledPath = `require.resolve(
|
|
'../compiled/dev-server/start-server'
|
|
)`
|
|
const unbundledPath = `require.resolve('../server/lib/start-server')`
|
|
|
|
let newContent
|
|
if (useBundled) {
|
|
newContent = content.replace(
|
|
/const startServerPath = require\.resolve\(['"]\.\.\/server\/lib\/start-server['"]\)/,
|
|
`const startServerPath = ${bundledPath}`
|
|
)
|
|
} else {
|
|
newContent = content.replace(
|
|
/const startServerPath = require\.resolve\(\s*['"]\.\.\/compiled\/dev-server\/start-server['"]\s*\)/,
|
|
`const startServerPath = ${unbundledPath}`
|
|
)
|
|
}
|
|
|
|
if (newContent !== content) {
|
|
fs.writeFileSync(cliSource, newContent)
|
|
// Rebuild CLI
|
|
console.log(`Rebuilding CLI (${useBundled ? 'bundled' : 'unbundled'})...`)
|
|
execSync('npx taskr cli', { cwd: nextDir, stdio: 'ignore' })
|
|
}
|
|
}
|
|
|
|
// Main
|
|
async function main() {
|
|
killNextDev()
|
|
|
|
if (compare) {
|
|
// Run both bundled and unbundled
|
|
setBundled(true)
|
|
const bundledResults = await runBenchmark('Bundled dev server')
|
|
|
|
setBundled(false)
|
|
const unbundledResults = await runBenchmark('Unbundled dev server')
|
|
|
|
// Restore to bundled
|
|
setBundled(true)
|
|
|
|
// Print comparison
|
|
console.log('\x1b[34m=== Comparison ===\x1b[0m')
|
|
if (bundledResults && unbundledResults) {
|
|
const reportedDiff =
|
|
bundledResults.reported.avg - unbundledResults.reported.avg
|
|
const actualDiff = bundledResults.actual.avg - unbundledResults.actual.avg
|
|
|
|
console.log(
|
|
`Reported time difference: ${reportedDiff > 0 ? '+' : ''}${reportedDiff}ms (${reportedDiff > 0 ? 'bundled slower' : 'bundled faster'})`
|
|
)
|
|
console.log(
|
|
`Actual time difference: ${actualDiff > 0 ? '+' : ''}${actualDiff}ms (${actualDiff > 0 ? 'bundled slower' : 'bundled faster'})`
|
|
)
|
|
}
|
|
} else {
|
|
await runBenchmark('Dev server')
|
|
}
|
|
|
|
console.log('\x1b[32mDone!\x1b[0m')
|
|
}
|
|
|
|
main().catch(console.error)
|