perf(frontend): lazy-load modals + split heavy vendor chunks (#4501)

* perf(frontend): lazy-load modals on inbounds / clients / index pages

Modals on the three list pages were imported statically, so the JS +
CSS for every form, info, qr, log, backup, metrics, system-history,
version, and config-text modal sat in the initial bundle even though
they're only needed after a click.

Converted those imports to React.lazy() and gated each modal with a
new LazyMount helper that mounts on first open and keeps the component
mounted thereafter so AntD close animations still play.

Build now emits a dedicated chunk per modal — InboundFormModal at
66 kB (13 kB gzipped) and InboundInfoModal at 23 kB (4 kB gzipped)
are the largest, totalling roughly 150 kB of code that no longer
parses on first paint. Profiler measured the inbounds-page React
render tree drop from ~444 ms to ~254 ms on a prod build.

* perf(frontend): split codemirror / jalali / otpauth into lazy vendor chunks

Heavy libs (codemirror, persian-calendar-suite, otpauth) and antd's
rc-/cssinjs transitive deps used to fall into the catch-all `vendor`
chunk and load with every entry point. Give them their own manualChunks
groups so they only load with the lazy modal/page that needs them.

Initial vendor (catch-all) drops from 1293 kB / 408 kB gzip to
76 kB / 27 kB gzip; codemirror (408 kB / 131 kB gzip) is now on the
JsonEditor lazy path instead of the inbounds/clients/index initial load.
This commit is contained in:
Sanaei
2026-05-23 18:56:11 +02:00
committed by GitHub
parent c5b71041d3
commit 09df07ddf5
5 changed files with 251 additions and 176 deletions
+86 -69
View File
@@ -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() {
</Layout.Content>
</Layout>
<PanelUpdateModal
open={panelUpdateOpen}
info={panelUpdateInfo}
onClose={() => setPanelUpdateOpen(false)}
onBusy={setBusy}
/>
<LogModal open={logsOpen} onClose={() => setLogsOpen(false)} />
<BackupModal
open={backupOpen}
basePath={basePath}
onClose={() => setBackupOpen(false)}
onBusy={setBusy}
/>
<SystemHistoryModal
open={sysHistoryOpen}
status={status}
onClose={() => setSysHistoryOpen(false)}
/>
<XrayMetricsModal open={xrayMetricsOpen} onClose={() => setXrayMetricsOpen(false)} />
<XrayLogModal open={xrayLogsOpen} onClose={() => setXrayLogsOpen(false)} />
<VersionModal
open={versionOpen}
status={status}
onClose={() => setVersionOpen(false)}
onBusy={setBusy}
/>
<Modal
open={configTextOpen}
title={t('pages.index.config')}
width={isMobile ? '100%' : 900}
style={isMobile ? { top: 20, maxWidth: 'calc(100vw - 16px)' } : undefined}
onCancel={() => setConfigTextOpen(false)}
footer={[
<Button
key="download"
onClick={downloadConfig}
size={isMobile ? 'small' : 'middle'}
icon={<CloudDownloadOutlined />}
>
{isMobile ? 'Download' : 'config.json'}
</Button>,
<Button
key="copy"
type="primary"
onClick={copyConfig}
size={isMobile ? 'small' : 'middle'}
icon={<CopyOutlined />}
>
Copy
</Button>,
]}
>
<JsonEditor
value={configText}
onChange={setConfigText}
minHeight={isMobile ? '300px' : '420px'}
maxHeight={isMobile ? '500px' : '720px'}
readOnly
<LazyMount when={panelUpdateOpen}>
<PanelUpdateModal
open={panelUpdateOpen}
info={panelUpdateInfo}
onClose={() => setPanelUpdateOpen(false)}
onBusy={setBusy}
/>
</Modal>
</LazyMount>
<LazyMount when={logsOpen}>
<LogModal open={logsOpen} onClose={() => setLogsOpen(false)} />
</LazyMount>
<LazyMount when={backupOpen}>
<BackupModal
open={backupOpen}
basePath={basePath}
onClose={() => setBackupOpen(false)}
onBusy={setBusy}
/>
</LazyMount>
<LazyMount when={sysHistoryOpen}>
<SystemHistoryModal
open={sysHistoryOpen}
status={status}
onClose={() => setSysHistoryOpen(false)}
/>
</LazyMount>
<LazyMount when={xrayMetricsOpen}>
<XrayMetricsModal open={xrayMetricsOpen} onClose={() => setXrayMetricsOpen(false)} />
</LazyMount>
<LazyMount when={xrayLogsOpen}>
<XrayLogModal open={xrayLogsOpen} onClose={() => setXrayLogsOpen(false)} />
</LazyMount>
<LazyMount when={versionOpen}>
<VersionModal
open={versionOpen}
status={status}
onClose={() => setVersionOpen(false)}
onBusy={setBusy}
/>
</LazyMount>
<LazyMount when={configTextOpen}>
<Modal
open={configTextOpen}
title={t('pages.index.config')}
width={isMobile ? '100%' : 900}
style={isMobile ? { top: 20, maxWidth: 'calc(100vw - 16px)' } : undefined}
onCancel={() => setConfigTextOpen(false)}
footer={[
<Button
key="download"
onClick={downloadConfig}
size={isMobile ? 'small' : 'middle'}
icon={<CloudDownloadOutlined />}
>
{isMobile ? 'Download' : 'config.json'}
</Button>,
<Button
key="copy"
type="primary"
onClick={copyConfig}
size={isMobile ? 'small' : 'middle'}
icon={<CopyOutlined />}
>
Copy
</Button>,
]}
>
<JsonEditor
value={configText}
onChange={setConfigText}
minHeight={isMobile ? '300px' : '420px'}
maxHeight={isMobile ? '500px' : '720px'}
readOnly
/>
</Modal>
</LazyMount>
</Layout>
</ConfigProvider>
);