Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ This repo currently resolves `vite` to `@voidzero-dev/vite-plus-core`, which bun
- Prefer `oxc` over `esbuild` for new JavaScript transform config
- Prefer `optimizeDeps.rolldownOptions` over `optimizeDeps.esbuildOptions`
- Prefer `build.rolldownOptions` / `worker.rolldownOptions` over adding new `*.rollupOptions` config
- When touching existing `build.rollupOptions` or `manualChunks`, preserve Vite 7 compatibility but treat them as migration targets, not patterns to copy forward
- When touching existing `build.rollupOptions` or `manualChunks`, treat them as migration targets to Rolldown equivalents, not patterns to copy forward
- If something breaks only on Vite 8, check the newer `build.target` baseline and stricter CommonJS default import behavior first

### Virtual Module Resolution Quirks
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ This will:

The migration is non-destructive -- your existing Next.js setup continues to work alongside vinext. It does not modify `next.config`, `tsconfig.json`, or any source files, and it does not remove Next.js dependencies.

vinext supports both Vite 7 and Vite 8. If you bring custom Vite config or plugins from an older setup, note that Vite 8 now defaults to Rolldown, Oxc, Lightning CSS, and a newer browser baseline. Prefer `oxc`, `optimizeDeps.rolldownOptions`, and `build.rolldownOptions` over older `esbuild` and `build.rollupOptions` knobs, and override `build.target` if you still need older browsers. If a dependency only breaks on Vite 8 because of stricter CommonJS default import handling, fix the import or use `legacy.inconsistentCjsInterop: true` as a temporary escape hatch. See the [Vite 8 migration guide](https://vite.dev/guide/migration).
vinext targets Vite 8, which defaults to Rolldown, Oxc, Lightning CSS, and a newer browser baseline. If you bring custom Vite config or plugins from an older setup, prefer `oxc`, `optimizeDeps.rolldownOptions`, and `build.rolldownOptions` over older `esbuild` and `build.rollupOptions` knobs, and override `build.target` if you still need older browsers. If a dependency breaks because of stricter CommonJS default import handling, fix the import or use `legacy.inconsistentCjsInterop: true` as a temporary escape hatch. See the [Vite 8 migration guide](https://vite.dev/guide/migration).

```bash
npm run dev:vinext # Start the vinext dev server (port 3001)
Expand Down Expand Up @@ -556,7 +556,7 @@ These are intentional exclusions:

These benchmarks measure **compilation and bundling speed**, not production serving performance. Next.js and vinext have fundamentally different default approaches: Next.js statically pre-renders pages at build time (making builds slower but production serving faster for static content), while vinext server-renders all pages on each request. To make the comparison apples-to-apples, the benchmark app uses `export const dynamic = "force-dynamic"` to disable Next.js static pre-rendering — both frameworks are doing the same work: compiling, bundling, and preparing server-rendered routes.

The benchmark app is a shared 33-route App Router application (server components, client components, dynamic routes, nested layouts, API routes) built identically by both tools. We compare Next.js 16 (Turbopack) against vinext on both Vite 7 (Rollup) and Vite 8 (Rolldown). Turbopack and Rolldown both parallelize across cores, so results on machines with more cores may differ significantly.
The benchmark app is a shared 33-route App Router application (server components, client components, dynamic routes, nested layouts, API routes) built identically by both tools. We compare Next.js (Turbopack) against vinext (Vite 8). Both Turbopack and Rolldown parallelize across cores, so results on machines with more cores may differ significantly.

We measure three things:

Expand Down
4 changes: 0 additions & 4 deletions benchmarks/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,18 @@ app/
# Copies of shared app in each project (generated by generate-app.mjs)
nextjs/app/
vinext/app/
vinext-rolldown/app/

# Build outputs
nextjs/.next/
vinext/dist/
vinext-rolldown/dist/

# Dependencies (each project has its own)
nextjs/node_modules/
vinext/node_modules/
vinext-rolldown/node_modules/

# Lock files
nextjs/package-lock.json
vinext/package-lock.json
vinext-rolldown/package-lock.json

# Next.js generated files
nextjs/next-env.d.ts
2 changes: 1 addition & 1 deletion benchmarks/generate-app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ console.log(
// Copy to each benchmark project (symlinks don't work with Turbopack)
import { cpSync } from "node:fs";
const BASE = dirname(new URL(import.meta.url).pathname);
for (const project of ["nextjs", "vinext", "vinext-rolldown"]) {
for (const project of ["nextjs", "vinext"]) {
const dest = join(BASE, project, "app");
rmSync(dest, { recursive: true, force: true });
cpSync(APP, dest, { recursive: true });
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"start": "next start"
},
"dependencies": {
"next": "16.1.7",
"next": "16.2.1",
"react": "19.2.4",
"react-dom": "19.2.4"
}
Expand Down
134 changes: 15 additions & 119 deletions benchmarks/run.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Benchmark harness: compares Next.js 16 (Turbopack) vs vinext (Vite 7/Rollup) vs vinext (Vite 8/Rolldown)
* Benchmark harness: compares Next.js (Turbopack) vs vinext (Vite 8)
*
* Metrics:
* 1. Production build time (hyperfine)
Expand All @@ -10,7 +10,7 @@
* 5. Memory usage (peak RSS during build and dev)
*
* Prerequisites: hyperfine, autocannon (npm i -g autocannon)
* Usage: node benchmarks/run.mjs [--runs N] [--dev-runs N] [--skip-build] [--skip-dev] [--skip-ssr] [--skip-rolldown]
* Usage: node benchmarks/run.mjs [--runs N] [--dev-runs N] [--skip-build] [--skip-dev] [--skip-ssr]
*/

import { execSync, spawn } from "node:child_process";
Expand All @@ -33,7 +33,6 @@ const DEV_RUNS = parseInt(args.find((a) => a.startsWith("--dev-runs="))?.split("
const SKIP_BUILD = args.includes("--skip-build");
const SKIP_DEV = args.includes("--skip-dev");
const SKIP_SSR = args.includes("--skip-ssr");
const SKIP_ROLLDOWN = args.includes("--skip-rolldown");

// ─── Helpers ───────────────────────────────────────────────────────────────────
function exec(cmd, opts = {}) {
Expand Down Expand Up @@ -228,13 +227,10 @@ async function main() {
},
nextjs: {},
vinext: {},
vinextRolldown: {},
};

const nextjsDir = join(__dirname, "nextjs");
const vinextDir = join(__dirname, "vinext");
const vinextRolldownDir = join(__dirname, "vinext-rolldown");
const hasRolldown = !SKIP_ROLLDOWN && existsSync(join(vinextRolldownDir, "package.json"));

// Detect actual installed versions for reproducibility
try {
Expand All @@ -253,16 +249,6 @@ async function main() {
} catch {
/* deps not installed yet */
}
if (hasRolldown) {
try {
const rdPkg = JSON.parse(
readFileSync(join(vinextRolldownDir, "node_modules", "vite", "package.json"), "utf-8"),
);
results.system.viteRolldownVersion = rdPkg.version;
} catch {
/* deps not installed yet */
}
}

// ─── 1. Production Build Time ──────────────────────────────────────────────
if (!SKIP_BUILD) {
Expand All @@ -271,7 +257,6 @@ async function main() {
// Clean previous builds
exec("rm -rf .next", { cwd: nextjsDir });
exec("rm -rf dist", { cwd: vinextDir });
if (hasRolldown) exec("rm -rf dist", { cwd: vinextRolldownDir });

// Ensure plugin is built
console.log(" Building vinext plugin...");
Expand All @@ -284,16 +269,10 @@ async function main() {
exec("./node_modules/.bin/next build --turbopack", { cwd: nextjsDir, timeout: 120000 });
exec("rm -rf .next", { cwd: nextjsDir });

console.log(" Warmup: vinext (Rollup) build...");
console.log(" Warmup: vinext build...");
exec("./node_modules/.bin/vp build", { cwd: vinextDir, timeout: 120000 });
exec("rm -rf dist", { cwd: vinextDir });

if (hasRolldown) {
console.log(" Warmup: vinext (Rolldown) build...");
exec("./node_modules/.bin/vp build", { cwd: vinextRolldownDir, timeout: 120000 });
exec("rm -rf dist", { cwd: vinextRolldownDir });
}

// Measured runs with hyperfine (single invocation with --shuffle for fair ordering)
console.log(`\n Running ${RUNS} build iterations with hyperfine (randomized order)...\n`);

Expand All @@ -314,11 +293,6 @@ async function main() {
`--command-name nextjs 'rm -rf ${nextjsDir}/.next && ./node_modules/.bin/next build --turbopack'`,
`--command-name vinext 'cd ${vinextDir} && rm -rf dist && ./node_modules/.bin/vp build'`,
];
if (hasRolldown) {
cmds.push(
`--command-name rolldown 'cd ${vinextRolldownDir} && rm -rf dist && ./node_modules/.bin/vp build'`,
);
}

console.log(" Timing all builds (shuffled)...");
const hfJson = exec(
Expand All @@ -333,15 +307,13 @@ async function main() {
results.nextjs.buildTime = parseHyperfineResult(r);
} else if (r.command.includes(vinextDir)) {
results.vinext.buildTime = parseHyperfineResult(r);
} else if (hasRolldown && r.command.includes(vinextRolldownDir)) {
results.vinextRolldown.buildTime = parseHyperfineResult(r);
}
}
results.buildMethodology = "hyperfine --shuffle (randomized)";
} catch {
// Fallback: manual timing with randomized runner order to eliminate positional bias
console.log(" hyperfine failed, falling back to manual timing...");
const buildTimes = { nextjs: [], vinext: [], rolldown: [] };
const buildTimes = { nextjs: [], vinext: [] };

const buildRunners = [
{
Expand All @@ -362,22 +334,6 @@ async function main() {
buildTimes.vinext.push(performance.now() - start);
},
},
...(hasRolldown
? [
{
key: "rolldown",
run: () => {
exec("rm -rf dist", { cwd: vinextRolldownDir });
const start = performance.now();
exec("./node_modules/.bin/vp build", {
cwd: vinextRolldownDir,
timeout: 120000,
});
buildTimes.rolldown.push(performance.now() - start);
},
},
]
: []),
];

const buildRunOrders = [];
Expand Down Expand Up @@ -407,14 +363,6 @@ async function main() {
min: Math.min(...buildTimes.vinext),
max: Math.max(...buildTimes.vinext),
};
if (hasRolldown && buildTimes.rolldown.length) {
results.vinextRolldown.buildTime = {
mean: avg(buildTimes.rolldown),
stddev: stddev(buildTimes.rolldown),
min: Math.min(...buildTimes.rolldown),
max: Math.max(...buildTimes.rolldown),
};
}
results.buildMethodology = "manual timing (randomized)";
results.buildRunOrders = buildRunOrders;
}
Expand All @@ -441,30 +389,19 @@ async function main() {
` Next.js: ${njsSize.files} files, ${formatBytes(njsSize.raw)} raw, ${formatBytes(njsSize.gzip)} gzip`,
);

// vinext (Rollup): client bundles are in dist/client
// vinext: client bundles are in dist/client
const ncSize = bundleSize(join(vinextDir, "dist", "client"));
results.vinext.bundleSize = ncSize;
console.log(
` vinext (Rollup): ${ncSize.files} files, ${formatBytes(ncSize.raw)} raw, ${formatBytes(ncSize.gzip)} gzip`,
` vinext: ${ncSize.files} files, ${formatBytes(ncSize.raw)} raw, ${formatBytes(ncSize.gzip)} gzip`,
);

// vinext (Rolldown): client bundles are in dist/client
if (hasRolldown) {
exec("rm -rf dist", { cwd: vinextRolldownDir });
exec("./node_modules/.bin/vp build", { cwd: vinextRolldownDir, timeout: 120000 });
const rdSize = bundleSize(join(vinextRolldownDir, "dist", "client"));
results.vinextRolldown.bundleSize = rdSize;
console.log(
` vinext (Rolldown): ${rdSize.files} files, ${formatBytes(rdSize.raw)} raw, ${formatBytes(rdSize.gzip)} gzip`,
);
}
}

// ─── 3. Dev Server Cold Start ──────────────────────────────────────────────
if (!SKIP_DEV) {
console.log("\n=== Dev Server Cold Start ===\n");

const devResults = { nextjs: [], vinext: [], rolldown: [] };
const devResults = { nextjs: [], vinext: [] };

// Define the runners; order is randomized each iteration to eliminate
// positional bias (first-after-kill penalties, OS cache warming, etc.)
Expand All @@ -484,7 +421,7 @@ async function main() {
},
{
key: "vinext",
label: "vinext (Rollup)",
label: "vinext",
run: async () => {
exec("rm -rf node_modules/.vite", { cwd: vinextDir });
return startAndMeasure(
Expand All @@ -495,23 +432,6 @@ async function main() {
);
},
},
...(hasRolldown
? [
{
key: "rolldown",
label: "vinext (Rolldown)",
run: async () => {
exec("rm -rf node_modules/.vite", { cwd: vinextRolldownDir });
return startAndMeasure(
"./node_modules/.bin/vp",
["dev", "--port", "4102"],
vinextRolldownDir,
"http://localhost:4102",
);
},
},
]
: []),
];

const runOrders = [];
Expand Down Expand Up @@ -549,13 +469,6 @@ async function main() {
meanRssKb: avg(devResults.vinext.map((r) => r.peakRssKb)),
runs: devResults.vinext,
};
if (hasRolldown && devResults.rolldown.length) {
results.vinextRolldown.devColdStart = {
meanMs: avg(devResults.rolldown.map((r) => r.coldStartMs)),
meanRssKb: avg(devResults.rolldown.map((r) => r.peakRssKb)),
runs: devResults.rolldown,
};
}
results.devRunOrders = runOrders;
}

Expand Down Expand Up @@ -586,16 +499,11 @@ async function main() {
md += `- **Build runs**: ${results.buildRuns}\n`;
md += `- **Dev cold start runs**: ${results.devRuns}\n`;
if (results.system.nextjsVersion) md += `- **Next.js**: ${results.system.nextjsVersion}\n`;
if (results.system.viteVersion) md += `- **Vite (Rollup)**: ${results.system.viteVersion}\n`;
if (results.system.viteRolldownVersion)
md += `- **Vite (Rolldown)**: ${results.system.viteRolldownVersion}\n`;
if (results.system.viteVersion) md += `- **Vite**: ${results.system.viteVersion}\n`;
md += "\n";
md += `> **Note:** TypeScript type checking is disabled for the Next.js build (\`typescript.ignoreBuildErrors: true\`) so that build timings measure bundler/compilation speed only. Vite does not type-check during build.\n\n`;
md += `> **Methodology:** Build and dev cold start runs are executed in randomized order to eliminate positional bias from filesystem caches, CPU thermal state, and residual process state.\n\n`;

const hasRolldownResults =
results.vinextRolldown && Object.keys(results.vinextRolldown).length > 0;

// Format a speed ratio comparison: "2.1x faster" or "1.3x slower"
function fmtSpeedup(baseline, value) {
if (!baseline || !value) return "N/A";
Expand All @@ -617,38 +525,26 @@ async function main() {
md += `## Production Build Time\n\n`;
md += `| Framework | Mean | StdDev | Min | Max | vs Next.js |\n`;
md += `|-----------|------|--------|-----|-----|------------|\n`;
md += `| Next.js 16 (Turbopack) | ${formatMs(results.nextjs.buildTime.mean)} | ±${formatMs(results.nextjs.buildTime.stddev)} | ${formatMs(results.nextjs.buildTime.min)} | ${formatMs(results.nextjs.buildTime.max)} | baseline |\n`;
md += `| vinext (Vite 7 / Rollup) | ${formatMs(results.vinext.buildTime.mean)} | ±${formatMs(results.vinext.buildTime.stddev)} | ${formatMs(results.vinext.buildTime.min)} | ${formatMs(results.vinext.buildTime.max)} | ${fmtSpeedup(results.nextjs.buildTime.mean, results.vinext.buildTime.mean)} |\n`;

if (hasRolldownResults && results.vinextRolldown.buildTime) {
md += `| vinext (Vite 8 / Rolldown) | ${formatMs(results.vinextRolldown.buildTime.mean)} | ±${formatMs(results.vinextRolldown.buildTime.stddev)} | ${formatMs(results.vinextRolldown.buildTime.min)} | ${formatMs(results.vinextRolldown.buildTime.max)} | ${fmtSpeedup(results.nextjs.buildTime.mean, results.vinextRolldown.buildTime.mean)} |\n`;
}
md += `| Next.js (Turbopack) | ${formatMs(results.nextjs.buildTime.mean)} | ±${formatMs(results.nextjs.buildTime.stddev)} | ${formatMs(results.nextjs.buildTime.min)} | ${formatMs(results.nextjs.buildTime.max)} | baseline |\n`;
md += `| vinext (Vite 8) | ${formatMs(results.vinext.buildTime.mean)} | ±${formatMs(results.vinext.buildTime.stddev)} | ${formatMs(results.vinext.buildTime.min)} | ${formatMs(results.vinext.buildTime.max)} | ${fmtSpeedup(results.nextjs.buildTime.mean, results.vinext.buildTime.mean)} |\n`;
md += "\n";
}

if (results.nextjs.bundleSize && results.vinext.bundleSize) {
md += `## Production Bundle Size (Client)\n\n`;
md += `| Framework | Files | Raw | Gzipped | vs Next.js (gzip) |\n`;
md += `|-----------|-------|-----|----------|--------------------|\n`;
md += `| Next.js 16 | ${results.nextjs.bundleSize.files} | ${formatBytes(results.nextjs.bundleSize.raw)} | ${formatBytes(results.nextjs.bundleSize.gzip)} | baseline |\n`;
md += `| vinext (Rollup) | ${results.vinext.bundleSize.files} | ${formatBytes(results.vinext.bundleSize.raw)} | ${formatBytes(results.vinext.bundleSize.gzip)} | ${fmtSizeReduction(results.nextjs.bundleSize.gzip, results.vinext.bundleSize.gzip)} |\n`;

if (hasRolldownResults && results.vinextRolldown.bundleSize) {
md += `| vinext (Rolldown) | ${results.vinextRolldown.bundleSize.files} | ${formatBytes(results.vinextRolldown.bundleSize.raw)} | ${formatBytes(results.vinextRolldown.bundleSize.gzip)} | ${fmtSizeReduction(results.nextjs.bundleSize.gzip, results.vinextRolldown.bundleSize.gzip)} |\n`;
}
md += `| Next.js (Turbopack) | ${results.nextjs.bundleSize.files} | ${formatBytes(results.nextjs.bundleSize.raw)} | ${formatBytes(results.nextjs.bundleSize.gzip)} | baseline |\n`;
md += `| vinext (Vite 8) | ${results.vinext.bundleSize.files} | ${formatBytes(results.vinext.bundleSize.raw)} | ${formatBytes(results.vinext.bundleSize.gzip)} | ${fmtSizeReduction(results.nextjs.bundleSize.gzip, results.vinext.bundleSize.gzip)} |\n`;
md += "\n";
}

if (results.nextjs.devColdStart && results.vinext.devColdStart) {
md += `## Dev Server Cold Start\n\n`;
md += `| Framework | Mean Cold Start | Mean Peak RSS | vs Next.js |\n`;
md += `|-----------|----------------|----------------|------------|\n`;
md += `| Next.js 16 (Turbopack) | ${formatMs(results.nextjs.devColdStart.meanMs)} | ${Math.round(results.nextjs.devColdStart.meanRssKb / 1024)} MB | baseline |\n`;
md += `| vinext (Vite 7 / Rollup) | ${formatMs(results.vinext.devColdStart.meanMs)} | ${Math.round(results.vinext.devColdStart.meanRssKb / 1024)} MB | ${fmtSpeedup(results.nextjs.devColdStart.meanMs, results.vinext.devColdStart.meanMs)} |\n`;

if (hasRolldownResults && results.vinextRolldown.devColdStart) {
md += `| vinext (Vite 8 / Rolldown) | ${formatMs(results.vinextRolldown.devColdStart.meanMs)} | ${Math.round(results.vinextRolldown.devColdStart.meanRssKb / 1024)} MB | ${fmtSpeedup(results.nextjs.devColdStart.meanMs, results.vinextRolldown.devColdStart.meanMs)} |\n`;
}
md += `| Next.js (Turbopack) | ${formatMs(results.nextjs.devColdStart.meanMs)} | ${Math.round(results.nextjs.devColdStart.meanRssKb / 1024)} MB | baseline |\n`;
md += `| vinext (Vite 8) | ${formatMs(results.vinext.devColdStart.meanMs)} | ${Math.round(results.vinext.devColdStart.meanRssKb / 1024)} MB | ${fmtSpeedup(results.nextjs.devColdStart.meanMs, results.vinext.devColdStart.meanMs)} |\n`;
md += "\n";
}

Expand Down
Loading
Loading