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/
.graphifyignore
graphify-out/*
.next-analyze/*
+42 -20
View File
@@ -4,8 +4,28 @@ const { spawn, exec, execSync } = require("child_process");
const path = require("path");
const fs = require("fs");
const https = require("https");
const net = require("net");
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
function createSpinner(text) {
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -212,17 +232,18 @@ function killCloudflaredByAppPort(appPort) {
function killAllAppProcesses(appPort) {
return new Promise((resolve) => {
try {
// Kill MIT first (privileged process, needs special handling)
killProxyByPidFile();
// Kill cloudflared/tailscale by PID file (precise, only this app's tunnel)
killTunnelByPidFile();
// Background: MITM + tunnel/cloudflared run on separate ports/processes —
// killing them doesn't free the app port, so don't block the critical path.
// Server-side MITM manager has stale-lock recovery and starts deferred (~3s).
setImmediate(() => {
try { killProxyByPidFile(); } catch {}
try { killTunnelByPidFile(); } catch {}
try { killCloudflaredByAppPort(appPort); } catch {}
});
const platform = process.platform;
let pids = [];
// Catch stale PID files: kill cloudflared bound to this app's port
pids.push(...killCloudflaredByAppPort(appPort));
if (platform === "win32") {
// Windows: use WMI to get full CommandLine (tasklist /V doesn't include it)
try {
@@ -499,14 +520,11 @@ if (!fs.existsSync(serverPath)) {
process.exit(1);
}
// Check for updates FIRST, then start server
checkForUpdate().then((latestVersion) => {
killAllAppProcesses(port).then(() => {
return killProcessOnPort(port);
}).then(() => {
startServer(latestVersion);
});
});
// Start server immediately; run update check in parallel (not on the critical path).
const updatePromise = checkForUpdate();
killAllAppProcesses(port)
.then(() => killProcessOnPort(port))
.then(() => startServer(updatePromise));
// Show interface selection menu
async function showInterfaceMenu(latestVersion) {
@@ -556,7 +574,9 @@ async function showInterfaceMenu(latestVersion) {
const MAX_RESTARTS = 2;
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 url = `http://${displayHost}:${port}/dashboard`;
// 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(`Server: http://${displayHost}:${port}`);
setTimeout(() => {
waitServerReady(port).then(() => {
initTrayIcon();
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");
}, 2000);
});
return;
}
// 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
initTrayIcon();
@@ -772,7 +794,7 @@ function startServer(latestVersion) {
cleanup();
process.exit(1);
}
}, 3000);
});
function attachServerEvents() {
server.on("error", (err) => {
+2
View File
@@ -30,6 +30,8 @@ const nextConfig = {
proxyClientMaxBodySize,
// Cache fetch responses across HMR refreshes for faster dev reloads.
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 }) => {
// Ignore fs/path modules in browser bundle
+2 -1
View File
@@ -4,7 +4,8 @@
"description": "9Router web dashboard",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 20127",
"dev": "next dev --port 20127",
"dev:webpack": "next dev --webpack --port 20127",
"build": "next build --webpack",
"start": "next start --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 *));
+3 -1
View File
@@ -14,7 +14,9 @@ import Badge from "./Badge";
import Card from "./Card";
import OverviewCards from "@/app/(dashboard)/dashboard/usage/components/OverviewCards";
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";
function timeAgo(timestamp) {