next.js / scripts /benchmark-next-dev-boot.js
AbdulElahGwaith's picture
Upload folder using huggingface_hub
b91e262 verified
#!/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)