diff --git a/frontend/src/components/LazyMount.tsx b/frontend/src/components/LazyMount.tsx
new file mode 100644
index 00000000..a1ffdd5c
--- /dev/null
+++ b/frontend/src/components/LazyMount.tsx
@@ -0,0 +1,20 @@
+import { Suspense, useEffect, useState, type ReactNode } from 'react';
+
+interface LazyMountProps {
+ when: boolean;
+ fallback?: ReactNode;
+ children: ReactNode;
+}
+
+// Mounts children only after `when` first becomes true and keeps them mounted
+// thereafter, so React.lazy modals get loaded on demand but their close
+// animations still play out. Pair with `lazy(() => import(...))` modal imports
+// on heavy list pages to keep the initial bundle small.
+export default function LazyMount({ when, fallback = null, children }: LazyMountProps) {
+ const [mounted, setMounted] = useState(when);
+ useEffect(() => {
+ if (when && !mounted) setMounted(true);
+ }, [when, mounted]);
+ if (!mounted) return null;
+ return {children};
+}
diff --git a/frontend/src/pages/clients/ClientsPage.tsx b/frontend/src/pages/clients/ClientsPage.tsx
index 06d015f7..6657aa79 100644
--- a/frontend/src/pages/clients/ClientsPage.tsx
+++ b/frontend/src/pages/clients/ClientsPage.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Badge,
@@ -51,11 +51,12 @@ import AppSidebar from '@/components/AppSidebar';
import CustomStatistic from '@/components/CustomStatistic';
import { IntlUtil, SizeFormatter } from '@/utils';
import { setMessageInstance } from '@/utils/messageBus';
-import ClientFormModal from './ClientFormModal';
-import ClientInfoModal from './ClientInfoModal';
-import ClientQrModal from './ClientQrModal';
-import ClientBulkAddModal from './ClientBulkAddModal';
-import ClientBulkAdjustModal from './ClientBulkAdjustModal';
+import LazyMount from '@/components/LazyMount';
+const ClientFormModal = lazy(() => import('./ClientFormModal'));
+const ClientInfoModal = lazy(() => import('./ClientInfoModal'));
+const ClientQrModal = lazy(() => import('./ClientQrModal'));
+const ClientBulkAddModal = lazy(() => import('./ClientBulkAddModal'));
+const ClientBulkAdjustModal = lazy(() => import('./ClientBulkAdjustModal'));
import '@/styles/page-cards.css';
import './ClientsPage.css';
@@ -853,51 +854,61 @@ export default function ClientsPage() {
-
-
-
- setBulkAddOpen(false)}
- />
- {
- const msg = await bulkAdjust([...selectedRowKeys], addDays, addBytes);
- if (msg?.success) {
- setSelectedRowKeys([]);
- return msg.obj ?? { adjusted: 0 };
- }
- return null;
- }}
- />
+
+
+
+
+
+
+
+
+
+
+ setBulkAddOpen(false)}
+ />
+
+
+ {
+ const msg = await bulkAdjust([...selectedRowKeys], addDays, addBytes);
+ if (msg?.success) {
+ setSelectedRowKeys([]);
+ return msg.obj ?? { adjusted: 0 };
+ }
+ return null;
+ }}
+ />
+
);
diff --git a/frontend/src/pages/inbounds/InboundsPage.tsx b/frontend/src/pages/inbounds/InboundsPage.tsx
index 27323fe4..ce3b12d1 100644
--- a/frontend/src/pages/inbounds/InboundsPage.tsx
+++ b/frontend/src/pages/inbounds/InboundsPage.tsx
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Card,
@@ -28,14 +28,15 @@ import { useWebSocket } from '@/hooks/useWebSocket';
import { useNodes } from '@/hooks/useNodes';
import AppSidebar from '@/components/AppSidebar';
import CustomStatistic from '@/components/CustomStatistic';
-import TextModal from '@/components/TextModal';
-import PromptModal from '@/components/PromptModal';
+const TextModal = lazy(() => import('@/components/TextModal'));
+const PromptModal = lazy(() => import('@/components/PromptModal'));
import { useInbounds } from './useInbounds';
import InboundList from './InboundList';
-import InboundFormModal from './InboundFormModal';
-import InboundInfoModal from './InboundInfoModal';
-import QrCodeModal from './QrCodeModal';
+import LazyMount from '@/components/LazyMount';
+const InboundFormModal = lazy(() => import('./InboundFormModal'));
+const InboundInfoModal = lazy(() => import('./InboundInfoModal'));
+const QrCodeModal = lazy(() => import('./QrCodeModal'));
import '@/styles/page-cards.css';
import './InboundsPage.css';
@@ -517,56 +518,66 @@ export default function InboundsPage() {
- setFormOpen(false)}
- onSaved={refresh}
- mode={formMode}
- dbInbound={formDbInbound}
- dbInbounds={dbInbounds as any[]}
- availableNodes={nodesList}
- />
- setInfoOpen(false)}
- dbInbound={infoDbInbound}
- clientIndex={infoClientIndex}
- remarkModel={remarkModel}
- expireDiff={expireDiff}
- trafficDiff={trafficDiff}
- ipLimitEnable={ipLimitEnable}
- tgBotEnable={tgBotEnable}
- subSettings={subSettings}
- lastOnlineMap={lastOnlineMap}
- nodeAddress={infoNodeAddress}
- />
- setQrOpen(false)}
- dbInbound={qrDbInbound}
- client={null}
- remarkModel={remarkModel}
- nodeAddress={qrNodeAddress}
- subSettings={subSettings}
- />
+
+ setFormOpen(false)}
+ onSaved={refresh}
+ mode={formMode}
+ dbInbound={formDbInbound}
+ dbInbounds={dbInbounds as any[]}
+ availableNodes={nodesList}
+ />
+
+
+ setInfoOpen(false)}
+ dbInbound={infoDbInbound}
+ clientIndex={infoClientIndex}
+ remarkModel={remarkModel}
+ expireDiff={expireDiff}
+ trafficDiff={trafficDiff}
+ ipLimitEnable={ipLimitEnable}
+ tgBotEnable={tgBotEnable}
+ subSettings={subSettings}
+ lastOnlineMap={lastOnlineMap}
+ nodeAddress={infoNodeAddress}
+ />
+
+
+ setQrOpen(false)}
+ dbInbound={qrDbInbound}
+ client={null}
+ remarkModel={remarkModel}
+ nodeAddress={qrNodeAddress}
+ subSettings={subSettings}
+ />
+
- setTextOpen(false)}
- title={textTitle}
- content={textContent}
- fileName={textFileName}
- />
- setPromptOpen(false)}
- title={promptTitle}
- okText={promptOkText}
- type={promptType}
- initialValue={promptInitial}
- loading={promptLoading}
- onConfirm={onPromptConfirm}
- />
+
+ setTextOpen(false)}
+ title={textTitle}
+ content={textContent}
+ fileName={textFileName}
+ />
+
+
+ setPromptOpen(false)}
+ title={promptTitle}
+ okText={promptOkText}
+ type={promptType}
+ initialValue={promptInitial}
+ loading={promptLoading}
+ onConfirm={onPromptConfirm}
+ />
+
);
diff --git a/frontend/src/pages/index/IndexPage.tsx b/frontend/src/pages/index/IndexPage.tsx
index 9d1ef77e..5334c171 100644
--- a/frontend/src/pages/index/IndexPage.tsx
+++ b/frontend/src/pages/index/IndexPage.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Button,
@@ -40,18 +40,19 @@ import { useStatus } from '@/hooks/useStatus';
import { useMediaQuery } from '@/hooks/useMediaQuery';
import AppSidebar from '@/components/AppSidebar';
import CustomStatistic from '@/components/CustomStatistic';
-import JsonEditor from '@/components/JsonEditor';
+import LazyMount from '@/components/LazyMount';
import { setMessageInstance } from '@/utils/messageBus';
import StatusCard from './StatusCard';
import XrayStatusCard from './XrayStatusCard';
-import PanelUpdateModal from './PanelUpdateModal';
import type { PanelUpdateInfo } from './PanelUpdateModal';
-import LogModal from './LogModal';
-import BackupModal from './BackupModal';
-import SystemHistoryModal from './SystemHistoryModal';
-import XrayMetricsModal from './XrayMetricsModal';
-import XrayLogModal from './XrayLogModal';
-import VersionModal from './VersionModal';
+const JsonEditor = lazy(() => import('@/components/JsonEditor'));
+const PanelUpdateModal = lazy(() => import('./PanelUpdateModal'));
+const LogModal = lazy(() => import('./LogModal'));
+const BackupModal = lazy(() => import('./BackupModal'));
+const SystemHistoryModal = lazy(() => import('./SystemHistoryModal'));
+const XrayMetricsModal = lazy(() => import('./XrayMetricsModal'));
+const XrayLogModal = lazy(() => import('./XrayLogModal'));
+const VersionModal = lazy(() => import('./VersionModal'));
import '@/styles/page-cards.css';
import './IndexPage.css';
@@ -435,67 +436,83 @@ export default function IndexPage() {
- setPanelUpdateOpen(false)}
- onBusy={setBusy}
- />
- setLogsOpen(false)} />
- setBackupOpen(false)}
- onBusy={setBusy}
- />
- setSysHistoryOpen(false)}
- />
- setXrayMetricsOpen(false)} />
- setXrayLogsOpen(false)} />
- setVersionOpen(false)}
- onBusy={setBusy}
- />
-
- setConfigTextOpen(false)}
- footer={[
- }
- >
- {isMobile ? 'Download' : 'config.json'}
- ,
- }
- >
- Copy
- ,
- ]}
- >
-
+ setPanelUpdateOpen(false)}
+ onBusy={setBusy}
/>
-
+
+
+ setLogsOpen(false)} />
+
+
+ setBackupOpen(false)}
+ onBusy={setBusy}
+ />
+
+
+ setSysHistoryOpen(false)}
+ />
+
+
+ setXrayMetricsOpen(false)} />
+
+
+ setXrayLogsOpen(false)} />
+
+
+ setVersionOpen(false)}
+ onBusy={setBusy}
+ />
+
+
+
+ setConfigTextOpen(false)}
+ footer={[
+ }
+ >
+ {isMobile ? 'Download' : 'config.json'}
+ ,
+ }
+ >
+ Copy
+ ,
+ ]}
+ >
+
+
+
);
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 5fc6f6c9..2b6b5056 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -174,7 +174,16 @@ export default defineConfig({
manualChunks(id) {
if (!id.includes('node_modules')) return undefined;
if (id.includes('/node_modules/antd/')) return 'vendor-antd';
- if (id.includes('/@ant-design/icons/')) return 'vendor-icons';
+ if (id.includes('/@ant-design/icons/') || id.includes('/@ant-design/icons-svg/')) return 'vendor-icons';
+ if (
+ id.includes('/node_modules/@rc-component/')
+ || id.includes('/node_modules/rc-')
+ || id.includes('/@ant-design/cssinjs')
+ || id.includes('/@ant-design/colors')
+ || id.includes('/@ant-design/fast-color')
+ || id.includes('/@ant-design/react-slick')
+ || id.includes('/@ctrl/tinycolor')
+ ) return 'vendor-antd';
if (
id.includes('/node_modules/react-i18next/')
|| id.includes('/node_modules/i18next/')
@@ -184,6 +193,13 @@ export default defineConfig({
|| id.includes('/node_modules/react-dom/')
|| id.includes('/node_modules/scheduler/')
) return 'vendor-react';
+ if (
+ id.includes('/node_modules/codemirror/')
+ || id.includes('/node_modules/@codemirror/')
+ || id.includes('/node_modules/@lezer/')
+ ) return 'vendor-codemirror';
+ if (id.includes('/node_modules/persian-calendar-suite/')) return 'vendor-jalali';
+ if (id.includes('/node_modules/otpauth/')) return 'vendor-otpauth';
if (id.includes('dayjs')) return 'vendor-dayjs';
if (id.includes('axios')) return 'vendor-axios';
return 'vendor';