Performance: Lazy-loading Heavy Dialogs & Panels (PERF-05)
Performance: Lazy-loading Heavy Dialogs & Panels (PERF-05)
Released in v1.0.53 — 2026-03-26
Overview
Release v1.0.53 completes the PERF-05 performance initiative. The goal is to reduce the amount of JavaScript that must be parsed and executed before the Budget and Service Charges dashboard routes become interactive.
Page-level code splitting (PERF-01) was already in place. This work goes one level deeper: heavy UI components that are only rendered when a user explicitly triggers an action (clicking a button or a table row) are now split into separate async chunks and loaded on demand.
Total uncompressed JS removed from initial bundles: ~128 KB.
How it works
Next.js's next/dynamic (nextDynamic) is used with { ssr: false } for purely client-side panels and dialogs. Each lazily loaded component receives a Skeleton fallback that is shown while the async chunk is fetching. On a CDN the load time is typically under 100 ms and is imperceptible to users.
// Example pattern used across both routes
const BudgetDetailPanel = nextDynamic(
() => import("./budget-detail-panel").then((m) => ({ default: m.BudgetDetailPanel })),
{ ssr: false, loading: () => <Skeleton className="h-64 w-full" /> }
);
Budget route
Saving: ~55 KB uncompressed
The original budget/client.tsx contained several large inline components. These are now extracted into dedicated files:
| File | Loaded when |
|---|---|
budget/create-budget-dialog.tsx | User clicks "New Annual Budget" |
budget/create-expenditure-dialog.tsx | User clicks "Record Expenditure" |
budget/budget-detail-panel.tsx | User clicks a budget row |
budget/sinking-fund-dialogs.tsx | User clicks any sinking fund action |
budget/client.tsx now imports all four via nextDynamic() and renders a Skeleton in their place until the chunk is ready.
Service Charges route
Saving: ~73 KB uncompressed
Two large detail panels were previously statically imported into service-charges/client.tsx:
| Component | Approx. source size | Change |
|---|---|---|
BudgetDetailPanel | ~41 KB | Converted to nextDynamic |
ArrearsDetailPanel | ~32 KB | Converted to nextDynamic |
A new service-charges/lazy-panels.tsx shim file provides named exports for both lazily loaded components, keeping import paths clean for consumers.
CreateBudgetDialog (~11 KB) remains a static import — it is small enough that splitting it would add round-trip overhead without a meaningful bundle benefit.
Middleware
The /api/health path is now excluded from the Next.js middleware matcher. Previously, the middleware ran on every health-check request even though no authentication or request processing was required. This reduces unnecessary overhead on monitoring and load-balancer probes.
What stays the same
- All dialogs, panels, and forms behave identically to before.
- No API contracts or tRPC procedure signatures changed.
- The
BudgetDetailPanelavailable inside the Budget route and the one surfaced through the Service Charges route are the same component, just imported differently. - Skeleton placeholder UI is shown only while the async chunk is loading; once cached by the browser the component renders immediately.
Impact summary
| Route | JS removed from initial load |
|---|---|
/dashboard/budget | ~55 KB |
/dashboard/service-charges | ~73 KB |
| Total | ~128 KB |
All savings are uncompressed figures. Gzip/Brotli compression ratios will reduce the over-the-wire delta further, but the parse/compile cost on the client is proportional to the uncompressed size.