Skip to content
Case study · Interface.ai

Five seconds to one: a page-load story

Profiling, ruthless bundle surgery, and the unglamorous work of making a banking platform feel instant.

5s → 1s

page load, p75

Role

Performance lead on the Unification platform

Timeframe

2024 – 2025

Stack

Lighthouse · Datadog · Code splitting · Module Federation

The problem

The main application loaded in 5–6 seconds, and bank operations staff felt it dozens of times a day. The cause was structural: each of the seven legacy apps bundled its own React instance and its own copies of every vendor library, and most screens made waterfall API calls on mount — each request waiting on the previous one for data it didn't actually depend on.

Nobody owned performance. It degraded one dependency and one API call at a time, which is exactly how platforms get slow.

The constraints

  • The fixes had to land inside the live consolidation effort — no separate 'performance rewrite' budget existed.
  • Seven teams shipping independently meant any shared-bundle strategy needed genuine buy-in, not a mandate.
  • Banking users on managed corporate machines — old hardware and locked-down browsers were the baseline, not the edge case.

Decisions & trade-offs

Measure first: Lighthouse + Datadog before touching code

I profiled real user sessions in Datadog and lab runs in Lighthouse to rank bottlenecks by actual cost: duplicated vendor bundles first, render-blocking CSS second, serial API calls third. The plan followed the data, not intuition.

Trade-off: Two weeks of measurement before any visible fix is a hard sell when everyone can feel the app is slow. It prevented us from optimizing the wrong things.

One shared vendor chunk across all microfrontends

React, the router, and heavyweight shared libraries moved into a single shared chunk loaded once by the shell. Seven copies of React became one.

Trade-off: Shared chunks mean version coupling — upgrading React now affects every micro-app simultaneously, so upgrades became coordinated platform events rather than per-team decisions.

Route-level code splitting with designed loading states

Each route loads only its own code. The loading states were designed deliberately — skeletons matched final layout to avoid flash-of-empty-content and layout shift.

Trade-off: Code splitting trades one big wait for many small ones. Done carelessly it feels worse; the UX treatment of every loading boundary was as much work as the splitting itself.

Parallelizing the API waterfall

Screens that made four sequential calls on mount were restructured to fire independent requests concurrently and stream results into the UI as they arrived.

Trade-off: Concurrent data arrival means the UI must handle any completion order — more states to design and test than a simple sequential load.

The outcome

Page load dropped from 5–6 seconds to 1–2 seconds. The Lighthouse performance score went from roughly 45 to 85+.

More durable than the numbers: performance became a platform property. New micro-apps inherit the shared chunk, the splitting pattern, and the loading-state conventions by default — the architecture keeps the platform fast without heroics.

Page load

Before
5–6s
After
1–2s

Lighthouse score

Before
~45
After
85+

What I'd do differently

I'd set up a performance budget in CI at the start — a hard bundle-size ceiling per remote would have prevented two regressions we later had to hunt down manually.

I'd also capture Core Web Vitals from real users from day one instead of relying on lab data early on. Field data reshaped our priorities once we had it; we should have had it sooner.