| | #!/usr/bin/env node |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | const { spawn, execSync } = require('child_process') |
| | const path = require('path') |
| | const fs = require('fs') |
| |
|
| | |
| | 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('') |
| |
|
| | |
| | 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) |
| | } |
| |
|
| | |
| | function killNextDev() { |
| | try { |
| | execSync('pkill -f "next dev"', { stdio: 'ignore' }) |
| | } catch {} |
| | } |
| |
|
| | |
| | function runIteration() { |
| | return new Promise((resolve, reject) => { |
| | |
| | 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() |
| | |
| | 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) |
| | } |
| | }) |
| |
|
| | |
| | setTimeout(() => { |
| | if (!resolved) { |
| | resolved = true |
| | child.kill('SIGKILL') |
| | reject(new Error('Timeout waiting for server to start')) |
| | } |
| | }, 60000) |
| | }) |
| | } |
| |
|
| | |
| | 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 |
| | } |
| |
|
| | |
| | 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 } |
| | } |
| |
|
| | |
| | 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) |
| | |
| | console.log(`Rebuilding CLI (${useBundled ? 'bundled' : 'unbundled'})...`) |
| | execSync('npx taskr cli', { cwd: nextDir, stdio: 'ignore' }) |
| | } |
| | } |
| |
|
| | |
| | async function main() { |
| | killNextDev() |
| |
|
| | if (compare) { |
| | |
| | setBundled(true) |
| | const bundledResults = await runBenchmark('Bundled dev server') |
| |
|
| | setBundled(false) |
| | const unbundledResults = await runBenchmark('Unbundled dev server') |
| |
|
| | |
| | setBundled(true) |
| |
|
| | |
| | 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) |
| |
|