From 15073f53d99055829257f5fe0750b7eb03196e55 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 23 Sep 2026 14:51:14 +0700 Subject: [PATCH] =?UTF-8?q?perf(web):=20=D1=80=D0=B0=D0=B7=D0=B1=D0=B8?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=BD=D0=B4=D0=BB=D0=B0?= =?UTF-8?q?=20=D0=B8=20runtime-=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - autoCodeSplitting роутов: начальный JS 1012КБ/298гз → 426КБ/131гз (−58%), админ-чанки грузятся по требованию - фильтры грида: убрана искусственная задержка 200мс - SystemMonitorPopover: поллинг 4 запросов/30с только при открытом поповере - /me в beforeLoad через ensureQueryData (кэш вместо запроса на каждую навигацию) - глобальный staleTime 30с (мутации инвалидируют кэш явно) - режим analyze + rollup-plugin-visualizer (pnpm --filter web analyze) --- .../layout/system-monitor-popover.tsx | 26 ++++++++++++++----- .../components/reui-kit/admin-users-grid.tsx | 19 +++----------- apps/web/src/main.tsx | 8 +++++- apps/web/src/routes/_auth.tsx | 10 ++++--- apps/web/vite.config.ts | 14 +++++++--- 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/apps/web/src/components/layout/system-monitor-popover.tsx b/apps/web/src/components/layout/system-monitor-popover.tsx index c3d9c8d..ea708d1 100644 --- a/apps/web/src/components/layout/system-monitor-popover.tsx +++ b/apps/web/src/components/layout/system-monitor-popover.tsx @@ -1,4 +1,4 @@ -import { useMemo, type ReactNode } from 'react' +import { useMemo, useState, type ReactNode } from 'react' import { useQuery } from '@tanstack/react-query' import { Activity, AppWindow, KeyRound, Users } from 'lucide-react' @@ -76,6 +76,9 @@ function MetricCell({ metric }: { metric: MonitorMetric }) { /** Live system monitor popover — app-shell-7, Auth Portal metrics. @see https://reui.io/preview/base/app-shell-7 */ export function SystemMonitorPopover() { + // Поллинг живёт только пока поповер открыт: закрытый монитор не должен + // каждые 30 секунд тянуть /ready, каталог и админские списки. + const [open, setOpen] = useState(false) const meQ = useQuery(meQueryOptions) const isAdmin = Boolean(meQ.data?.is_admin) @@ -86,24 +89,33 @@ export function SystemMonitorPopover() { if (!res.ok) throw new Error('not ready') return (await res.json()) as { ok?: boolean } }, + enabled: open, refetchInterval: 30_000, retry: false, }) - const catalogQ = useQuery({ ...catalogQueryOptions, refetchInterval: 30_000 }) + const catalogQ = useQuery({ + ...catalogQueryOptions, + enabled: open, + refetchInterval: 30_000, + }) const usersQ = useQuery({ ...usersQueryOptions, - enabled: isAdmin, + enabled: isAdmin && open, retry: false, refetchInterval: 30_000, }) const oidcQ = useQuery({ ...oidcClientsQueryOptions, - enabled: isAdmin, + enabled: isAdmin && open, retry: false, refetchInterval: 30_000, }) - const apiOk = readyQ.data?.ok === true || readyQ.isSuccess + const monitorFetched = readyQ.isFetched || catalogQ.isFetched + // До первого открытия данных нет — нейтральное состояние вместо ложной тревоги. + const apiOk = readyQ.isFetched + ? readyQ.data?.ok === true || readyQ.isSuccess + : true const appsCount = catalogQ.data?.apps.length ?? 0 const usersCount = isAdmin ? (usersQ.data?.length ?? 0) : null const oidcCount = isAdmin ? (oidcQ.data?.length ?? 0) : null @@ -163,10 +175,10 @@ export function SystemMonitorPopover() { [apiOk, appsCount, disabledUsers, oidcCount, usersCount], ) - const spiking = metrics.some((m) => m.alert) || !apiOk + const spiking = monitorFetched && (metrics.some((m) => m.alert) || !apiOk) return ( - + (createDefaultFilters) const [bulkRole, setBulkRole] = useState('user') const [filteredData, setFilteredData] = useState(users) - const [filterLoading, setFilterLoading] = useState(false) const isInitialLoad = useRef(true) const lastAppliedActiveKey = useRef( serializeActiveFiltersKey(getActiveFilters(createDefaultFilters())), @@ -851,16 +850,6 @@ export function AdminUsersGrid({ [users], ) - const simulateAsyncFiltering = useCallback( - async (newFilters: Filter[]) => { - setFilterLoading(true) - await new Promise((resolve) => setTimeout(resolve, 200)) - setFilteredData(applyFilters(newFilters)) - setFilterLoading(false) - }, - [applyFilters], - ) - const handleFiltersChange = useCallback( (newFilters: Filter[]) => { setFilters(newFilters) @@ -870,9 +859,8 @@ export function AdminUsersGrid({ lastAppliedActiveKey.current = nextKey setPagination((prev) => ({ ...prev, pageIndex: 0 })) setRowSelection({}) - void simulateAsyncFiltering(newFilters) }, - [simulateAsyncFiltering], + [], ) useEffect(() => { @@ -933,7 +921,7 @@ export function AdminUsersGrid({ .getSelectedRowModel() .rows.map((row) => row.original.id) const showClearButton = filters.length > 0 - const gridLoading = isLoading || filterLoading + const gridLoading = isLoading const handleClearSelection = useCallback(() => setRowSelection({}), []) @@ -956,8 +944,7 @@ export function AdminUsersGrid({ ) setFilters(next) setRowSelection({}) - void simulateAsyncFiltering(next) - }, [simulateAsyncFiltering]) + }, []) return ( diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 4d7a20a..844c0e4 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -9,7 +9,13 @@ import '@authportal/ui/globals.css' const queryClient = new QueryClient({ defaultOptions: { - queries: { retry: 1, refetchOnWindowFocus: false }, + queries: { + retry: 1, + refetchOnWindowFocus: false, + // Данные портала меняются редко; мутации инвалидируют кэш явно. + // Без staleTime каждый маунт компонента рефетчил /me, каталог и т.п. + staleTime: 30_000, + }, }, }) diff --git a/apps/web/src/routes/_auth.tsx b/apps/web/src/routes/_auth.tsx index 0336801..9130532 100644 --- a/apps/web/src/routes/_auth.tsx +++ b/apps/web/src/routes/_auth.tsx @@ -6,17 +6,18 @@ import { jwtClaimsStaleVsMe, reissueAccessToken, } from '@/lib/auth' -import { api } from '@/lib/api-client' -import type { MeResponse } from '@authportal/shared' +import { meQueryKey, meQueryOptions } from '@/queries/auth' export const Route = createFileRoute('/_auth')({ - beforeLoad: async ({ location }) => { + beforeLoad: async ({ context: { queryClient }, location }) => { const token = getToken() if (!token) { throw redirect({ to: '/' }) } try { - const me = await api.get('/api/v1/auth/me') + // Кэшированный /me вместо сырого запроса на каждый вход в auth-дерево; + // сайдбар, NavUser и монитор читают тот же кэш (staleTime из QueryClient). + const me = await queryClient.ensureQueryData(meQueryOptions) if (jwtClaimsStaleVsMe(token, me)) { await reissueAccessToken() } @@ -25,6 +26,7 @@ export const Route = createFileRoute('/_auth')({ } } catch (err) { if (err && typeof err === 'object' && 'to' in err) throw err + queryClient.removeQueries({ queryKey: meQueryKey }) clearToken() throw redirect({ to: '/' }) } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 70543fc..bafa262 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -3,12 +3,20 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' import { TanStackRouterVite } from '@tanstack/router-vite-plugin' +import { visualizer } from 'rollup-plugin-visualizer' -export default defineConfig({ +export default defineConfig(({ mode }) => ({ plugins: [ - TanStackRouterVite({ routesDirectory: './src/routes' }), + TanStackRouterVite({ + routesDirectory: './src/routes', + autoCodeSplitting: true, + }), react(), tailwindcss(), + // pnpm --filter web analyze → dist/stats.html + ...(mode === 'analyze' + ? [visualizer({ filename: 'dist/stats.html', gzipSize: true })] + : []), ], resolve: { alias: { @@ -32,4 +40,4 @@ export default defineConfig({ '/oauth': 'http://localhost:8080', }, }, -}) +}))