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