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={[ - , - , - ]} - > - + setPanelUpdateOpen(false)} + onBusy={setBusy} /> - + + + setLogsOpen(false)} /> + + + setBackupOpen(false)} + onBusy={setBusy} + /> + + + setSysHistoryOpen(false)} + /> + + + setXrayMetricsOpen(false)} /> + + + setXrayLogsOpen(false)} /> + + + setVersionOpen(false)} + onBusy={setBusy} + /> + + + + setConfigTextOpen(false)} + footer={[ + , + , + ]} + > + + + ); 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';