perf(accounts): defer the account ledger prop on show
The show page loaded the full transaction ledger into the initial Inertia payload on every visit — several MB for accounts with years of history, all on the critical render path. Defer the transactions prop so the page shell paints immediately and the ledger arrives in a follow-up request behind a skeleton. It stays the whole set on purpose: search and filtering run client-side over decrypted rows, so cursor pagination would break search over encrypted fields.
This commit is contained in:
parent
021cb66643
commit
c0ad01cfe3
|
|
@ -120,20 +120,20 @@ class AccountController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
// ponytail: load-all transactions for the account (server-side, replaces
|
||||
// the old client-side sync-all-then-filter). Fine for a single account;
|
||||
// switch to a deferred/cursor-paginated prop if accounts grow huge.
|
||||
$transactions = $account->type->hasTransactionLedger()
|
||||
? $account->transactions()
|
||||
->with(['category', 'labels'])
|
||||
->orderBy('transaction_date', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->get()
|
||||
: [];
|
||||
|
||||
return Inertia::render('Accounts/Show', [
|
||||
'account' => $data,
|
||||
'transactions' => $transactions,
|
||||
// Deferred so the page shell paints without blocking on the ledger
|
||||
// query/serialization. It stays the whole set because search and
|
||||
// filtering run client-side over decrypted rows. ponytail: window it
|
||||
// server-side only if one account's history gets big enough that the
|
||||
// transfer itself hurts.
|
||||
'transactions' => $account->type->hasTransactionLedger()
|
||||
? Inertia::defer(fn () => $account->transactions()
|
||||
->with(['category', 'labels'])
|
||||
->orderBy('transaction_date', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->get())
|
||||
: [],
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import AccountShow from './Show';
|
|||
vi.mock('@inertiajs/react', () => ({
|
||||
Head: () => null,
|
||||
router: { reload: vi.fn() },
|
||||
Deferred: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
}));
|
||||
|
||||
vi.mock('@/actions/App/Http/Controllers/AccountController', () => ({
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useChartColors } from '@/hooks/use-chart-color-scheme';
|
||||
import AppSidebarLayout from '@/layouts/app/app-sidebar-layout';
|
||||
|
|
@ -63,7 +64,7 @@ import { Label as LabelType } from '@/types/label';
|
|||
import { type Transaction } from '@/types/transaction';
|
||||
import { formatDateMedium } from '@/utils/date';
|
||||
import { __ } from '@/utils/i18n';
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import { Deferred, Head, router } from '@inertiajs/react';
|
||||
import { ChevronDown, Pencil, Plus } from 'lucide-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Line, LineChart, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
|
|
@ -382,23 +383,34 @@ export default function AccountShow({
|
|||
)}
|
||||
|
||||
{isTransactionalAccount(account) && (
|
||||
<TransactionList
|
||||
categories={categories}
|
||||
accounts={accounts}
|
||||
banks={banks}
|
||||
labels={labels}
|
||||
automationRules={automationRules}
|
||||
accountId={account.id}
|
||||
transactions={transactions}
|
||||
pageSize={50}
|
||||
hideAccountFilter={true}
|
||||
showActionsMenu={false}
|
||||
maxHeight={600}
|
||||
hideColumns={['bank', 'account']}
|
||||
onBalanceUpdated={() =>
|
||||
setChartRefreshKey((key) => key + 1)
|
||||
<Deferred
|
||||
data="transactions"
|
||||
fallback={
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<TransactionList
|
||||
categories={categories}
|
||||
accounts={accounts}
|
||||
banks={banks}
|
||||
labels={labels}
|
||||
automationRules={automationRules}
|
||||
accountId={account.id}
|
||||
transactions={transactions}
|
||||
pageSize={50}
|
||||
hideAccountFilter={true}
|
||||
showActionsMenu={false}
|
||||
maxHeight={600}
|
||||
hideColumns={['bank', 'account']}
|
||||
onBalanceUpdated={() =>
|
||||
setChartRefreshKey((key) => key + 1)
|
||||
}
|
||||
/>
|
||||
</Deferred>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -238,7 +238,10 @@ test('account show includes the account transactions and excludes other accounts
|
|||
->assertOk()
|
||||
->assertInertia(fn ($page) => $page
|
||||
->component('Accounts/Show')
|
||||
->has('transactions', 2)
|
||||
->missing('transactions')
|
||||
->loadDeferredProps(fn ($reload) => $reload
|
||||
->has('transactions', 2)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue