Skip to main content
All Docs
FeaturesBlockManOSUpdated March 26, 2026

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:

FileLoaded when
budget/create-budget-dialog.tsxUser clicks "New Annual Budget"
budget/create-expenditure-dialog.tsxUser clicks "Record Expenditure"
budget/budget-detail-panel.tsxUser clicks a budget row
budget/sinking-fund-dialogs.tsxUser 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:

ComponentApprox. source sizeChange
BudgetDetailPanel~41 KBConverted to nextDynamic
ArrearsDetailPanel~32 KBConverted 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 BudgetDetailPanel available 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

RouteJS 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.