perf(arayüz): sayfa bazlı kod bölme, oturum isteği paralel, ana sayfaları önden yükleme

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-13 23:19:57 +03:00
parent 57272a4c7f
commit 8ee3672d57
7 changed files with 152 additions and 57 deletions
+43 -1
View File
@@ -1,5 +1,6 @@
import { useEffect } from 'react'
import { LogOut } from 'lucide-react'
import { NavLink, Outlet, ScrollRestoration, useLocation } from 'react-router'
import { NavLink, Outlet, ScrollRestoration, useLocation, useNavigation } from 'react-router'
import { Logo } from '@/components/logo'
import { ThemeToggle } from '@/components/theme-toggle'
import { Button } from '@/components/ui/button'
@@ -14,8 +15,10 @@ import { bottomNav, moreNav, paths, sideNav } from './nav-items'
* - Masaüstü (≥ md): solda kenar çubuğu
*/
export function AppLayout() {
usePrefetchMainPages()
return (
<div className="min-h-dvh">
<NavigationProgress />
<Sidebar />
{/* Mobil üst çubuk */}
@@ -37,6 +40,45 @@ export function AppLayout() {
)
}
/**
* Alt menüdeki ana sayfaların kodu, tarayıcı boşta kalınca önceden indirilir
* (ilk açılışı yavaşlatmaz; menüde gezinirken bekleme olmaz).
*/
function usePrefetchMainPages() {
useEffect(() => {
const load = () => {
void import('@/features/dashboard/page')
void import('@/features/ledger/page')
void import('@/features/planning/page')
void import('@/features/savings/page')
}
if ('requestIdleCallback' in window) {
const id = window.requestIdleCallback(load, { timeout: 5000 })
return () => window.cancelIdleCallback(id)
}
const id = setTimeout(load, 2000)
return () => clearTimeout(id)
}, [])
}
/** Sayfa kodu inerken (yavaş bağlantı) üstte ince ilerleme çizgisi; anlık geçişte görünmez */
function NavigationProgress() {
const loading = useNavigation().state === 'loading'
return (
<div
role="progressbar"
aria-label={t.common.loading}
aria-hidden={!loading}
className={cn(
'pointer-events-none fixed inset-x-0 top-0 z-50 h-0.5 origin-left bg-primary transition-[transform,opacity] print:hidden',
loading
? 'scale-x-75 opacity-100 delay-150 duration-[2s] ease-out'
: 'scale-x-100 opacity-0 duration-300',
)}
/>
)
}
function Sidebar() {
return (
<aside className="fixed inset-y-0 left-0 z-30 hidden w-64 flex-col border-r bg-sidebar md:flex print:hidden">
+12 -6
View File
@@ -6,7 +6,7 @@
* - `null` → oturum yok
* - `MeResponse`→ giriş yapılmış
*/
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'
import type { LoginRequest } from '@/types/LoginRequest'
import type { MeResponse } from '@/types/MeResponse'
import type { RegisterRequest } from '@/types/RegisterRequest'
@@ -23,12 +23,18 @@ async function fetchMe(): Promise<MeResponse | null> {
}
}
const meQuery = { queryKey: queryKeys.me, queryFn: fetchMe, staleTime: 5 * 60_000 }
export function useMe() {
return useQuery({
queryKey: queryKeys.me,
queryFn: fetchMe,
staleTime: 5 * 60_000,
})
return useQuery(meQuery)
}
/**
* Açılışta oturumu hemen sor: sayfa kodu (ayrı JS parçası) inerken istek paralel yürür,
* sıralı bekleme olmaz. `useMe` aynı sorguyu önbellekten alır.
*/
export function prefetchMe(client: QueryClient) {
void client.prefetchQuery(meQuery)
}
export function useLogin() {
+2
View File
@@ -5,11 +5,13 @@ import { RouterProvider } from 'react-router'
import './index.css'
import { queryClient } from '@/lib/query'
import { Toaster } from '@/components/toaster'
import { prefetchMe } from '@/lib/auth'
import { installPrintTheme } from '@/lib/print'
import { ThemeProvider } from '@/lib/theme'
import { router } from './router'
installPrintTheme()
prefetchMe(queryClient)
createRoot(document.getElementById('root')!).render(
<StrictMode>
+89 -46
View File
@@ -1,66 +1,106 @@
import type { ComponentType } from 'react'
import { createBrowserRouter, type RouteObject } from 'react-router'
import { AppLayout } from '@/components/layout/app-layout'
import { paths } from '@/components/layout/nav-items'
import { RequireAuth, RequireGuest } from '@/components/layout/require-auth'
import { LoginPage } from '@/features/auth/login-page'
import { RegisterPage } from '@/features/auth/register-page'
import { DashboardPage } from '@/features/dashboard/page'
import { DesignPreview } from '@/features/dev/design-preview'
import { NotFoundPage } from '@/features/errors/not-found-page'
import { LedgerPage } from '@/features/ledger/page'
import { MorePage } from '@/features/more/page'
import { PeriodWizardPage } from '@/features/period-wizard/page'
import { PlanningPage } from '@/features/planning/page'
import { ReportsPage } from '@/features/reports/page'
import { ClosePeriodPage } from '@/features/review/close-page'
import { MonthlyReportPage } from '@/features/review/report-page'
import { ReviewPage } from '@/features/review/page'
import { AssetPage } from '@/features/savings/asset-page'
import { SavingsPage } from '@/features/savings/page'
import { CategoriesPage, PaymentMethodsPage } from '@/features/settings/catalog-pages'
import { AccountPage } from '@/features/settings/account-page'
import { BudgetDefaultsPage } from '@/features/settings/budget-page'
import { DataPage } from '@/features/settings/data-page'
import { HouseholdPage } from '@/features/settings/household-page'
import { SettingsPage } from '@/features/settings/page'
import { StatementPage } from '@/features/statement/page'
import { RequireAuth, RequireGuest, Splash } from '@/components/layout/require-auth'
/**
* Sayfalar ayrı JS parçaları: yalnızca açılınca indirilir (ilk açılış küçük kalır).
* React Router `lazy`: yeni sayfanın kodu inerken eski sayfa ekranda kalır.
*/
const page =
<K extends string>(load: () => Promise<Record<K, ComponentType>>, name: K) =>
async () => ({ Component: (await load())[name] })
const routes: RouteObject[] = [
// Oturumsuz
{
element: <RequireGuest />,
HydrateFallback: Splash,
children: [
{ path: paths.login, element: <LoginPage /> },
{ path: paths.register, element: <RegisterPage /> },
{ path: paths.login, lazy: page(() => import('@/features/auth/login-page'), 'LoginPage') },
{
path: paths.register,
lazy: page(() => import('@/features/auth/register-page'), 'RegisterPage'),
},
],
},
// Oturumlu (iskelet: kenar çubuğu / alt menü)
{
element: <RequireAuth />,
HydrateFallback: Splash,
children: [
{
element: <AppLayout />,
children: [
{ index: true, element: <DashboardPage /> },
{ path: paths.ledger, element: <LedgerPage /> },
{ path: paths.planning, element: <PlanningPage /> },
{ path: paths.periodWizard, element: <PeriodWizardPage /> },
{ path: paths.savings, element: <SavingsPage /> },
{ path: paths.asset, element: <AssetPage /> },
{ path: paths.review, element: <ReviewPage /> },
{ path: paths.closePeriod, element: <ClosePeriodPage /> },
{ path: paths.monthlyReport, element: <MonthlyReportPage /> },
{ path: paths.reports, element: <ReportsPage /> },
{ path: paths.settings, element: <SettingsPage /> },
{ path: paths.settingsCategories, element: <CategoriesPage /> },
{ path: paths.settingsPaymentMethods, element: <PaymentMethodsPage /> },
{ path: paths.settingsData, element: <DataPage /> },
{ path: paths.settingsHousehold, element: <HouseholdPage /> },
{ path: paths.settingsAccount, element: <AccountPage /> },
{ path: paths.settingsBudget, element: <BudgetDefaultsPage /> },
{ path: paths.statement, element: <StatementPage /> },
{ path: paths.more, element: <MorePage /> },
{ path: '*', element: <NotFoundPage /> },
{ index: true, lazy: page(() => import('@/features/dashboard/page'), 'DashboardPage') },
{ path: paths.ledger, lazy: page(() => import('@/features/ledger/page'), 'LedgerPage') },
{
path: paths.planning,
lazy: page(() => import('@/features/planning/page'), 'PlanningPage'),
},
{
path: paths.periodWizard,
lazy: page(() => import('@/features/period-wizard/page'), 'PeriodWizardPage'),
},
{
path: paths.savings,
lazy: page(() => import('@/features/savings/page'), 'SavingsPage'),
},
{
path: paths.asset,
lazy: page(() => import('@/features/savings/asset-page'), 'AssetPage'),
},
{ path: paths.review, lazy: page(() => import('@/features/review/page'), 'ReviewPage') },
{
path: paths.closePeriod,
lazy: page(() => import('@/features/review/close-page'), 'ClosePeriodPage'),
},
{
path: paths.monthlyReport,
lazy: page(() => import('@/features/review/report-page'), 'MonthlyReportPage'),
},
{
path: paths.reports,
lazy: page(() => import('@/features/reports/page'), 'ReportsPage'),
},
{
path: paths.settings,
lazy: page(() => import('@/features/settings/page'), 'SettingsPage'),
},
{
path: paths.settingsCategories,
lazy: page(() => import('@/features/settings/catalog-pages'), 'CategoriesPage'),
},
{
path: paths.settingsPaymentMethods,
lazy: page(() => import('@/features/settings/catalog-pages'), 'PaymentMethodsPage'),
},
{
path: paths.settingsData,
lazy: page(() => import('@/features/settings/data-page'), 'DataPage'),
},
{
path: paths.settingsHousehold,
lazy: page(() => import('@/features/settings/household-page'), 'HouseholdPage'),
},
{
path: paths.settingsAccount,
lazy: page(() => import('@/features/settings/account-page'), 'AccountPage'),
},
{
path: paths.settingsBudget,
lazy: page(() => import('@/features/settings/budget-page'), 'BudgetDefaultsPage'),
},
{
path: paths.statement,
lazy: page(() => import('@/features/statement/page'), 'StatementPage'),
},
{ path: paths.more, lazy: page(() => import('@/features/more/page'), 'MorePage') },
{
path: '*',
lazy: page(() => import('@/features/errors/not-found-page'), 'NotFoundPage'),
},
],
},
],
@@ -69,7 +109,10 @@ const routes: RouteObject[] = [
// Tasarım önizlemesi yalnızca geliştirmede (canlı derlemeye girmez)
if (import.meta.env.DEV) {
routes.unshift({ path: '/dev/tasarim', element: <DesignPreview /> })
routes.unshift({
path: '/dev/tasarim',
lazy: page(() => import('@/features/dev/design-preview'), 'DesignPreview'),
})
}
export const router = createBrowserRouter(routes)