Created
November 26, 2025 22:29
-
-
Save pbzona/87b5b9b3d3f45993bb9fb8e7506d72dc to your computer and use it in GitHub Desktop.
ESM import benchmark
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Module Import Performance Benchmark (ESM) | |
| * | |
| * Measures ESM dynamic import() time for any TypeScript/JavaScript file. | |
| * Runs 100 iterations in fresh Node.js subprocesses to simulate cold-start. | |
| * Uses SWC to parse imports and separately benchmarks each dependency. | |
| * | |
| * Output: | |
| * - Average and p95 import time (100 runs) | |
| * - NPM package dependencies with individual times | |
| * - Local imports (@/ and ./) with individual times | |
| * - Export count and importer count | |
| * - JSON summary for sharing | |
| * | |
| * Usage: | |
| * pnpm tsx scripts/benchmark-import.ts <file-path> | |
| * | |
| * Examples: | |
| * pnpm tsx scripts/benchmark-import.ts src/db/schema.ts | |
| * pnpm tsx scripts/benchmark-import.ts src/lib/user.ts | |
| */ | |
| import { execSync } from 'child_process'; | |
| import { cpus } from 'os'; | |
| import { readFileSync, statSync } from 'fs'; | |
| import { resolve, relative, basename, dirname } from 'path'; | |
| import { parseSync } from '@swc/core'; | |
| const WARMUP_RUNS = 3; | |
| const BENCHMARK_RUNS = 100; | |
| interface TargetFile { | |
| absolutePath: string; | |
| relativePath: string; | |
| fileName: string; | |
| requirePath: string; | |
| } | |
| function parseArgs(): TargetFile { | |
| const args = process.argv.slice(2); | |
| if (args.length === 0) { | |
| console.error('Usage: pnpm tsx scripts/benchmark-import.ts <file-path>'); | |
| process.exit(1); | |
| } | |
| const inputPath = args[0]; | |
| const absolutePath = resolve(process.cwd(), inputPath); | |
| const relativePath = relative(process.cwd(), absolutePath); | |
| try { | |
| statSync(absolutePath); | |
| } catch { | |
| console.error(`Error: File not found: ${absolutePath}`); | |
| process.exit(1); | |
| } | |
| return { | |
| absolutePath, | |
| relativePath, | |
| fileName: basename(relativePath), | |
| requirePath: './' + relativePath.replace(/\.tsx?$/, ''), | |
| }; | |
| } | |
| function formatMs(ms: number): string { | |
| return `${ms.toFixed(1)}ms`; | |
| } | |
| interface ImportInfo { | |
| path: string; | |
| type: 'npm' | 'local'; | |
| resolvePath: string; // Path to use with import() | |
| } | |
| function extractRuntimeImports(filePath: string): ImportInfo[] { | |
| const content = readFileSync(filePath, 'utf-8'); | |
| const imports: ImportInfo[] = []; | |
| const seen = new Set<string>(); | |
| // Use SWC to parse the file | |
| const ast = parseSync(content, { | |
| syntax: 'typescript', | |
| tsx: filePath.endsWith('.tsx'), | |
| }); | |
| for (const item of ast.body) { | |
| // Skip type-only imports (ImportDeclaration with typeOnly: true) | |
| if (item.type === 'ImportDeclaration' && !item.typeOnly) { | |
| const importPath = item.source.value; | |
| if (importPath.startsWith('@/')) { | |
| // Local alias import (@/lib/foo -> ./src/lib/foo) | |
| if (!seen.has(importPath)) { | |
| seen.add(importPath); | |
| const localPath = './src/' + importPath.slice(2); | |
| imports.push({ path: importPath, type: 'local', resolvePath: localPath }); | |
| } | |
| } else if (importPath.startsWith('.')) { | |
| // Relative import (./foo, ../bar) | |
| if (!seen.has(importPath)) { | |
| seen.add(importPath); | |
| const fileDir = dirname(filePath); | |
| const resolvedPath = './' + relative(process.cwd(), resolve(fileDir, importPath)); | |
| imports.push({ path: importPath, type: 'local', resolvePath: resolvedPath }); | |
| } | |
| } else { | |
| // npm package - dedupe by package name (e.g., drizzle-orm/pg-core -> drizzle-orm) | |
| const packageName = importPath.startsWith('@') | |
| ? importPath.split('/').slice(0, 2).join('/') | |
| : importPath.split('/')[0]; | |
| if (!seen.has(packageName)) { | |
| seen.add(packageName); | |
| imports.push({ path: packageName, type: 'npm', resolvePath: packageName }); | |
| } | |
| } | |
| } | |
| } | |
| return imports; | |
| } | |
| function runBenchmark(modulePath: string, runs: number): number[] { | |
| const results: number[] = []; | |
| // Use ESM dynamic import() to match "import * as schema" behavior | |
| // This is closer to how Next.js/SWC processes imports | |
| const script = ` | |
| console.log=console.error=console.warn=()=>{}; | |
| const s=performance.now(); | |
| import('${modulePath}').then(m=>{ | |
| process.stdout.write(String(performance.now()-s)); | |
| }).catch(()=>{ | |
| process.stdout.write('0'); | |
| }); | |
| `.replace(/\n\s*/g, ''); | |
| for (let i = 0; i < runs; i++) { | |
| try { | |
| const output = execSync(`node --experimental-vm-modules -e "${script}"`, { | |
| cwd: process.cwd(), | |
| encoding: 'utf-8', | |
| stdio: ['pipe', 'pipe', 'pipe'], | |
| env: { ...process.env, NODE_OPTIONS: '--import tsx' }, | |
| }); | |
| const time = parseFloat(output.trim()); | |
| if (!isNaN(time) && time > 0) results.push(time); | |
| } catch { | |
| // Skip failed runs | |
| } | |
| } | |
| return results; | |
| } | |
| function stats(runs: number[]): { | |
| mean: number; | |
| p50: number; | |
| p95: number; | |
| min: number; | |
| max: number; | |
| } { | |
| if (runs.length === 0) return { mean: 0, p50: 0, p95: 0, min: 0, max: 0 }; | |
| const sorted = [...runs].sort((a, b) => a - b); | |
| const mean = runs.reduce((a, b) => a + b, 0) / runs.length; | |
| const p50 = sorted[Math.floor(sorted.length * 0.5)]; | |
| const p95 = sorted[Math.floor(sorted.length * 0.95)]; | |
| const min = sorted[0]; | |
| const max = sorted[sorted.length - 1]; | |
| return { mean, p50, p95, min, max }; | |
| } | |
| function countImporters(targetFile: TargetFile): number { | |
| if (!targetFile.relativePath.startsWith('src/')) return 0; | |
| const aliasPath = targetFile.relativePath.replace(/^src\//, '@/').replace(/\.tsx?$/, ''); | |
| try { | |
| const output = execSync( | |
| `grep -r "from '${aliasPath}'" src --include='*.ts' --include='*.tsx' 2>/dev/null | wc -l`, | |
| { cwd: process.cwd(), encoding: 'utf-8' } | |
| ); | |
| return parseInt(output.trim()) || 0; | |
| } catch { | |
| return 0; | |
| } | |
| } | |
| async function main() { | |
| const target = parseArgs(); | |
| const fileContent = readFileSync(target.absolutePath, 'utf-8'); | |
| const fileStats = statSync(target.absolutePath); | |
| const lines = fileContent.split('\n').length; | |
| const sizeKB = Math.round((fileStats.size / 1024) * 10) / 10; | |
| console.log(`\nπ¦ ${target.relativePath}`); | |
| console.log(` ${lines} lines, ${sizeKB} KB\n`); | |
| // Cold-start benchmark - detect if file errors on load (using ESM import) | |
| const checkScript = ` | |
| const c=console.log;console.log=console.error=console.warn=()=>{}; | |
| import('${target.requirePath}').then(()=>c('OK')).catch(e=>c('ERR:'+e.message)); | |
| `.replace(/\n\s*/g, ''); | |
| let loadError: string | null = null; | |
| try { | |
| const checkResult = | |
| execSync(`node --experimental-vm-modules -e "${checkScript}"`, { | |
| cwd: process.cwd(), | |
| encoding: 'utf-8', | |
| stdio: ['pipe', 'pipe', 'pipe'], | |
| env: { ...process.env, NODE_OPTIONS: '--import tsx' }, | |
| }) | |
| .trim() | |
| .split('\n') | |
| .pop() || ''; | |
| if (checkResult.startsWith('ERR:')) { | |
| loadError = checkResult.slice(4); | |
| } | |
| } catch { | |
| loadError = 'Unknown error'; | |
| } | |
| let runs: number[] = []; | |
| if (loadError) { | |
| console.log( | |
| ` β οΈ File errors on load: ${loadError.slice(0, 50)}${loadError.length > 50 ? '...' : ''}` | |
| ); | |
| console.log(' Using dependency times as estimate.\n'); | |
| } else { | |
| process.stdout.write(` Benchmarking (${BENCHMARK_RUNS} runs)`); | |
| for (let i = 0; i < WARMUP_RUNS; i++) { | |
| runBenchmark(target.requirePath, 1); | |
| } | |
| for (let i = 0; i < BENCHMARK_RUNS; i++) { | |
| const result = runBenchmark(target.requirePath, 1); | |
| if (result.length > 0) runs.push(result[0]); | |
| if (i % 20 === 0) process.stdout.write('.'); | |
| } | |
| process.stdout.write(' done\n\n'); | |
| } | |
| // Dependency breakdown | |
| const deps = extractRuntimeImports(target.absolutePath); | |
| const depTimes: { name: string; time: number; type: 'npm' | 'local' }[] = []; | |
| if (deps.length > 0) { | |
| for (const dep of deps) { | |
| const depRuns = runBenchmark(dep.resolvePath, 5); | |
| const depMean = depRuns.length > 0 ? depRuns.reduce((a, b) => a + b, 0) / depRuns.length : -1; | |
| depTimes.push({ name: dep.path, time: depMean, type: dep.type }); | |
| } | |
| } | |
| // Count exports (suppress console output) | |
| let exportCount = 0; | |
| const origLog = console.log; | |
| const origErr = console.error; | |
| const origWarn = console.warn; | |
| try { | |
| console.log = console.error = console.warn = () => {}; | |
| const importPath = '../' + target.relativePath.replace(/\.tsx?$/, ''); | |
| const mod = await import(importPath); | |
| exportCount = Object.keys(mod).length; | |
| } catch { | |
| // Ignore | |
| } finally { | |
| console.log = origLog; | |
| console.error = origErr; | |
| console.warn = origWarn; | |
| } | |
| const importerCount = countImporters(target); | |
| const { mean, p50, p95, min, max } = stats(runs); | |
| // Calculate totals for npm and local deps | |
| const npmDeps = depTimes.filter(d => d.type === 'npm' && d.time > 0); | |
| const localDeps = depTimes.filter(d => d.type === 'local' && d.time > 0); | |
| const totalDepTime = depTimes.reduce((sum, d) => sum + (d.time > 0 ? d.time : 0), 0); | |
| // If file errored, estimate from deps | |
| const estimatedFromDeps = !!(loadError && runs.length === 0 && totalDepTime > 0); | |
| const displayMean = estimatedFromDeps ? totalDepTime : mean; | |
| const displayP50 = estimatedFromDeps ? totalDepTime : p50; | |
| const displayP95 = estimatedFromDeps ? totalDepTime : p95; | |
| const displayMin = estimatedFromDeps ? totalDepTime : min; | |
| const displayMax = estimatedFromDeps ? totalDepTime : max; | |
| // Output | |
| if (displayMean > 0) { | |
| const label = estimatedFromDeps ? 'Estimated total' : 'Total import time (includes all deps)'; | |
| console.log(` ${label}:`); | |
| console.log(` ββββββββββ¬βββββββββ¬βββββββββ¬βββββββββ¬βββββββββ`); | |
| console.log(` β min β p50 β avg β p95 β max β`); | |
| console.log(` ββββββββββΌβββββββββΌβββββββββΌβββββββββΌβββββββββ€`); | |
| console.log( | |
| ` β${formatMs(displayMin).padStart(7)} β${formatMs(displayP50).padStart(7)} β${formatMs(displayMean).padStart(7)} β${formatMs(displayP95).padStart(7)} β${formatMs(displayMax).padStart(7)} β` | |
| ); | |
| console.log(` ββββββββββ΄βββββββββ΄βββββββββ΄βββββββββ΄βββββββββ`); | |
| console.log(); | |
| } | |
| if (npmDeps.length > 0 || localDeps.length > 0) { | |
| console.log(' Dependencies (measured in isolation):'); | |
| for (const dep of [...npmDeps, ...localDeps].sort((a, b) => b.time - a.time)) { | |
| const tag = dep.type === 'local' ? ' (local)' : ''; | |
| console.log(` ${(dep.name + tag).padEnd(36)} ${formatMs(dep.time).padStart(8)}`); | |
| } | |
| console.log(); | |
| } | |
| if (exportCount > 0 || importerCount > 0) { | |
| console.log(' Stats:'); | |
| if (exportCount > 0) console.log(` Exports: ${exportCount}`); | |
| if (importerCount > 0) console.log(` Imported by: ~${importerCount} files`); | |
| console.log(); | |
| } | |
| // JSON for sharing | |
| const summary = { | |
| file: target.relativePath, | |
| lines, | |
| sizeKB, | |
| exports: exportCount, | |
| importedBy: importerCount, | |
| avgMs: Math.round(displayMean * 10) / 10, | |
| p50Ms: Math.round(displayP50 * 10) / 10, | |
| p95Ms: Math.round(displayP95 * 10) / 10, | |
| minMs: Math.round(displayMin * 10) / 10, | |
| maxMs: Math.round(displayMax * 10) / 10, | |
| estimated: estimatedFromDeps, | |
| npmDeps: npmDeps.map(d => ({ name: d.name, ms: Math.round(d.time * 10) / 10 })), | |
| localDeps: localDeps.map(d => ({ name: d.name, ms: Math.round(d.time * 10) / 10 })), | |
| system: `${process.platform}/${process.arch}, Node ${process.version}, ${cpus()[0]?.model || 'Unknown CPU'}`, | |
| timestamp: new Date().toISOString(), | |
| }; | |
| console.log(' JSON:'); | |
| console.log(' ' + JSON.stringify(summary)); | |
| console.log(); | |
| } | |
| main().catch(console.error); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment