Skip to main content
All Docs
FeaturesBlockManOSUpdated March 26, 2026

Shrinking the JavaScript Bundle: How We Tackled Large Eager Client Components

Shrinking the JavaScript Bundle: How We Tackled Large Eager Client Components

Release: v1.0.52
Performance Control: PERF-05


The Problem

As the dashboard grew to support the full range of Irish block management workflows — budgeting, GDPR compliance, bank reconciliation, sinking funds, planned preventative maintenance, and more — each of those features accumulated significant client-side code. Without code splitting, every one of these modules was bundled together and sent to the browser on the very first page load, regardless of which section the user actually intended to visit.

An audit of the dashboard client components revealed the following uncompressed source sizes:

ComponentSize
budget/client.tsx72 KB
irish-legislation/client.tsx66 KB
gdpr/client.tsx64 KB
bank-reconciliation/client.tsx62 KB
sinking-fund/client.tsx58 KB
ppm/client.tsx54 KB
maintenance/client.tsx53 KB

The total uncompressed source across dashboard client components exceeded 700 KB. For a property management agent opening the dashboard to review a single service charge run, the browser was still parsing and compiling code for GDPR tools, legislation references, and PPM schedules they had no intention of using.


The Fix: Two Layers of Code Splitting

1. Route-Level Dynamic Imports (PERF-01)

The primary resolution comes from PERF-01, which introduces dynamic imports across the dashboard. Instead of being statically bundled at build time, each large client component is now split into its own chunk. The browser only downloads a component's JavaScript when the user first navigates to that route.

This means a user landing on the Maintenance section no longer pays the download and parse cost of the Budget or Irish Legislation modules.

2. Sub-Component Lazy Loading

Route splitting alone doesn't tell the whole story. Within certain large components, there are secondary views — detail panels, modals, and dialogs — that are only shown in response to a specific user action. These were also identified as candidates for lazy loading:

  • Arrears Detail Panel (service-charges/arrears-detail-panel.tsx, 32 KB): Only rendered when an agent drills into a specific arrears record. Now deferred until that interaction occurs.
  • Budget Detail Panel (budget-detail-panel.tsx, 41 KB): Only rendered when a budget line item is expanded for detailed review. Now deferred in the same way.

By extracting these into lazily-loaded sub-components, their code is excluded from even the route-level chunk, and only fetched at the moment the user triggers the relevant UI.


What This Means in Practice

  • Faster initial load. The JavaScript delivered on dashboard entry is now scoped to what is actually needed on the landing view.
  • Lower parse and compile cost. Browsers spend less time processing JavaScript before the page becomes interactive.
  • Better Time to Interactive (TTI). Agents can begin working sooner after navigating to any dashboard section.
  • Detail panels don't cost anything until they're needed. If an agent never opens the arrears detail panel in a session, that 32 KB is never downloaded at all.

Developer Notes

If you are extending the dashboard with new client components, follow these guidelines to avoid reintroducing bundle bloat:

  1. Use dynamic imports for all route-level client components. Any file named client.tsx under src/app/dashboard/ should be imported dynamically.
  2. Audit new components for lazy-loadable sub-components. Modals, detail panels, and dialogs that are gated behind user interaction are strong candidates. A component above 30 KB that is not shown on initial render should be treated as a lazy-loading candidate.
  3. Reference PERF-01 for the established dynamic import pattern used across this codebase.

This change is part of an ongoing series of frontend performance improvements to ensure the platform remains fast and responsive as the feature set scales.