mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 05:31:47 +00:00
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:
@@ -77,3 +77,4 @@ gitbook/README.md
|
||||
open-sse.old/
|
||||
.graphifyignore
|
||||
graphify-out/*
|
||||
.next-analyze/*
|
||||
|
||||
+42
-20
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 *));
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user