perf: faster dev startup and lighter bundle

- Switch dev default to Turbopack (5-14x faster compile); keep webpack as dev:webpack
- Tailwind v4 source() base so JIT scans identically under both bundlers
- Lazy-load @xyflow/react via next/dynamic to keep it out of the shared bundle
- optimizePackageImports for heavy barrel imports (xyflow, dnd-kit, material-symbols, marked)
- Replace blind setTimeout waits with TCP health-check (waitServerReady)
- Run checkForUpdate in parallel instead of blocking server spawn
- Background MITM/tunnel/cloudflared kills off the critical path

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-07-10 13:08:02 +07:00
co-authored by Cursor
parent ce6bdf7fc2
commit 0270f6ea70
6 changed files with 52 additions and 23 deletions
+1
View File
@@ -77,3 +77,4 @@ gitbook/README.md
open-sse.old/ open-sse.old/
.graphifyignore .graphifyignore
graphify-out/* graphify-out/*
.next-analyze/*
+42 -20
View File
@@ -4,8 +4,28 @@ const { spawn, exec, execSync } = require("child_process");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const https = require("https"); const https = require("https");
const net = require("net");
const os = require("os"); const os = require("os");
// Poll until the server accepts TCP connections on port, or timeout — avoids blind fixed waits.
function waitServerReady(port, { timeoutMs = 15000, intervalMs = 150 } = {}) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve) => {
const tryConnect = () => {
const socket = net.connect({ host: "127.0.0.1", port }, () => {
socket.destroy();
resolve(true);
});
socket.on("error", () => {
socket.destroy();
if (Date.now() >= deadline) return resolve(false);
setTimeout(tryConnect, intervalMs);
});
};
tryConnect();
});
}
// Native spinner - no external dependency // Native spinner - no external dependency
function createSpinner(text) { function createSpinner(text) {
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -212,17 +232,18 @@ function killCloudflaredByAppPort(appPort) {
function killAllAppProcesses(appPort) { function killAllAppProcesses(appPort) {
return new Promise((resolve) => { return new Promise((resolve) => {
try { try {
// Kill MIT first (privileged process, needs special handling) // Background: MITM + tunnel/cloudflared run on separate ports/processes —
killProxyByPidFile(); // killing them doesn't free the app port, so don't block the critical path.
// Kill cloudflared/tailscale by PID file (precise, only this app's tunnel) // Server-side MITM manager has stale-lock recovery and starts deferred (~3s).
killTunnelByPidFile(); setImmediate(() => {
try { killProxyByPidFile(); } catch {}
try { killTunnelByPidFile(); } catch {}
try { killCloudflaredByAppPort(appPort); } catch {}
});
const platform = process.platform; const platform = process.platform;
let pids = []; let pids = [];
// Catch stale PID files: kill cloudflared bound to this app's port
pids.push(...killCloudflaredByAppPort(appPort));
if (platform === "win32") { if (platform === "win32") {
// Windows: use WMI to get full CommandLine (tasklist /V doesn't include it) // Windows: use WMI to get full CommandLine (tasklist /V doesn't include it)
try { try {
@@ -499,14 +520,11 @@ if (!fs.existsSync(serverPath)) {
process.exit(1); process.exit(1);
} }
// Check for updates FIRST, then start server // Start server immediately; run update check in parallel (not on the critical path).
checkForUpdate().then((latestVersion) => { const updatePromise = checkForUpdate();
killAllAppProcesses(port).then(() => { killAllAppProcesses(port)
return killProcessOnPort(port); .then(() => killProcessOnPort(port))
}).then(() => { .then(() => startServer(updatePromise));
startServer(latestVersion);
});
});
// Show interface selection menu // Show interface selection menu
async function showInterfaceMenu(latestVersion) { async function showInterfaceMenu(latestVersion) {
@@ -556,7 +574,9 @@ async function showInterfaceMenu(latestVersion) {
const MAX_RESTARTS = 2; const MAX_RESTARTS = 2;
const RESTART_RESET_MS = 30000; // Reset counter if alive > 30s const RESTART_RESET_MS = 30000; // Reset counter if alive > 30s
function startServer(latestVersion) { function startServer(updatePromise) {
// Accept either a Promise (parallel update check) or a resolved value.
const latestVersionPromise = Promise.resolve(updatePromise);
const displayHost = getDisplayHost(); const displayHost = getDisplayHost();
const url = `http://${displayHost}:${port}/dashboard`; const url = `http://${displayHost}:${port}/dashboard`;
// Surface real network exposure when bound to all interfaces (default 0.0.0.0). // Surface real network exposure when bound to all interfaces (default 0.0.0.0).
@@ -677,17 +697,19 @@ function startServer(latestVersion) {
console.log(`\n🚀 ${pkg.name} v${pkg.version}`); console.log(`\n🚀 ${pkg.name} v${pkg.version}`);
console.log(`Server: http://${displayHost}:${port}`); console.log(`Server: http://${displayHost}:${port}`);
setTimeout(() => { waitServerReady(port).then(() => {
initTrayIcon(); initTrayIcon();
console.log("\n💡 Router is now running in system tray. Close this terminal if you want."); console.log("\n💡 Router is now running in system tray. Close this terminal if you want.");
console.log(" Right-click tray icon to open dashboard or quit.\n"); console.log(" Right-click tray icon to open dashboard or quit.\n");
}, 2000); });
return; return;
} }
// Wait for server to be ready, then show interface menu loop + tray // Wait for server to be ready, then show interface menu loop + tray
setTimeout(async () => { waitServerReady(port).then(async () => {
// Resolve parallel update check (already running); don't block server start on it.
const latestVersion = await latestVersionPromise;
// Start tray icon alongside TUI // Start tray icon alongside TUI
initTrayIcon(); initTrayIcon();
@@ -772,7 +794,7 @@ function startServer(latestVersion) {
cleanup(); cleanup();
process.exit(1); process.exit(1);
} }
}, 3000); });
function attachServerEvents() { function attachServerEvents() {
server.on("error", (err) => { server.on("error", (err) => {
+2
View File
@@ -30,6 +30,8 @@ const nextConfig = {
proxyClientMaxBodySize, proxyClientMaxBodySize,
// Cache fetch responses across HMR refreshes for faster dev reloads. // Cache fetch responses across HMR refreshes for faster dev reloads.
serverComponentsHmrCache: true, serverComponentsHmrCache: true,
// Tree-shake heavy barrel imports to cut compile + bundle size
optimizePackageImports: ["@xyflow/react", "@dnd-kit/core", "@dnd-kit/sortable", "material-symbols", "marked"],
}, },
webpack: (config, { isServer }) => { webpack: (config, { isServer }) => {
// Ignore fs/path modules in browser bundle // Ignore fs/path modules in browser bundle
+2 -1
View File
@@ -4,7 +4,8 @@
"description": "9Router web dashboard", "description": "9Router web dashboard",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --webpack --port 20127", "dev": "next dev --port 20127",
"dev:webpack": "next dev --webpack --port 20127",
"build": "next build --webpack", "build": "next build --webpack",
"start": "next start --port 20127", "start": "next start --port 20127",
"dev:bun": "bun --bun next dev --webpack --port 20127", "dev:bun": "bun --bun next dev --webpack --port 20127",
+2 -1
View File
@@ -1,4 +1,5 @@
@import "tailwindcss"; /* source() sets scan base to src/ for both webpack + Turbopack; auto-detection still skips binaries + gitignore */
@import "tailwindcss" source("../../");
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
+3 -1
View File
@@ -14,7 +14,9 @@ import Badge from "./Badge";
import Card from "./Card"; import Card from "./Card";
import OverviewCards from "@/app/(dashboard)/dashboard/usage/components/OverviewCards"; import OverviewCards from "@/app/(dashboard)/dashboard/usage/components/OverviewCards";
import UsageTable, { fmt, fmtTime } from "@/app/(dashboard)/dashboard/usage/components/UsageTable"; import UsageTable, { fmt, fmtTime } from "@/app/(dashboard)/dashboard/usage/components/UsageTable";
import ProviderTopology from "@/app/(dashboard)/dashboard/usage/components/ProviderTopology"; import dynamic from "next/dynamic";
// Lazy-load: keeps @xyflow/react out of the shared bundle until topology renders
const ProviderTopology = dynamic(() => import("@/app/(dashboard)/dashboard/usage/components/ProviderTopology"), { ssr: false });
import UsageChart from "@/app/(dashboard)/dashboard/usage/components/UsageChart"; import UsageChart from "@/app/(dashboard)/dashboard/usage/components/UsageChart";
function timeAgo(timestamp) { function timeAgo(timestamp) {