A logistics company came to us with a dispatch dashboard that took eight seconds to load and a customer success team that was losing accounts over it. Their previous agency had quoted a full rebuild. We took a smaller approach to web app performance optimization instead, and this is the breakdown.
Most web app performance optimization work looks like this one. The product is not broken. It is a product that grew. The question is whether you fix the specific bottlenecks or replace the whole thing, and that question deserves numbers, not instinct.
Situation, goal and constraint
The dashboard showed every active shipment for a customer with status, location and exceptions, refreshed every minute. It had been built four years earlier on a PHP back end and a React front end, both still supported. As customers grew from hundreds to thousands of shipments, the page went from fast to unusable. The goal was a dashboard that loaded in under three seconds for the largest customer. The constraint was that the operations team could not have the product offline, and the budget was a fraction of the rebuild quote.
Both constraints shaped the plan. No downtime meant every change had to ship behind the live product, with a rollback path, during normal deploy windows. A small budget meant we could not afford to fix things that were not slow. So the first job was to find out exactly where the eight seconds went before touching any code.
What we found in the first week
The first week was read-only: monitoring installed, the slowest requests traced, the front-end bundle analysed. The findings, in order of impact:
- The shipment query had no index on the customer and status columns. Every load scanned the whole table. Over three seconds on its own.
- An N+1 pattern loaded each shipment's latest location in a separate query. Two thousand shipments meant two thousand queries. Another two seconds.
- The front end shipped a 4 MB chart library for one sparkline per row. A second and a half on a normal connection, more on a laptop in a warehouse.
- The page requested everything at once and rendered nothing until all of it arrived.
None of this was the framework. All of it was the kind of thing that happens when a product succeeds faster than its code was built for.
Read-only matters here. In week one we change nothing in production except adding monitoring. That keeps risk near zero and gives a clean baseline to measure against later. The client received a short written report at the end of the week: each problem, the evidence for it, the expected saving, and a price to fix it. A good audit ranks problems by measured time. A bad audit lists every code smell it can find and leaves you to guess which ones matter.
What we built: four web app performance optimization fixes
Five weeks, two engineers, the same stack. Sprint one: indexes on the shipment table and a single query with a join for the latest location, verified with the query planner. Sprint two: the chart library replaced with a 6 KB inline sparkline, and the bundle split so the map loads after the table. Sprint three: the page loads the first fifty shipments immediately and streams the rest, with the filters moved server-side. Sprint four: load testing against a copy of the largest customer's data, and a cache for the status counts that drove the summary tiles. Sprint five: handover, documentation and a runbook for the operations team.
The index in sprint one was a composite one, with the customer column first because every dashboard query filters on it. In plain SQL it looks like this, and works in both MySQL and PostgreSQL: CREATE INDEX idx_shipments_customer_status ON shipments (customer_id, status); Column order is the detail people get wrong. An index on status alone would barely help, because most rows share a handful of status values.
"Verified with the query planner" means we ran EXPLAIN on the real query before and after, and confirmed the plan changed from a full table scan to an index lookup. We did not trust a faster timing on a developer laptop. Small local datasets hide exactly these problems.
Sprints were one week each. Each one ended with a deploy to production and a fresh measurement against the week-one baseline. If a change did not move the number, it did not count as progress, however clean the code looked.
The hard decision
The previous quote was a rebuild on a new framework. We priced it too, honestly: four months and roughly five times the fix budget, with the same four problems to solve inside the new code anyway. The client was wary of our smaller number because the last small number had not worked. We offered a condition: if the first sprint did not take a second off the load time, we would stop and charge for the week only. It took three seconds off.
The reasoning behind the condition was simple. The audit had already measured the index problem at over three seconds. We were not gambling. We were putting the evidence where the client could check it within a week, at a cost they could absorb if we were wrong. A rebuild asks for trust up front for months. A fix-first plan earns it one sprint at a time.
Results
Load time for the largest customer went from eight seconds to just over three, a 60 percent cut, measured at the 95th percentile with real traffic. The minute-by-minute refresh dropped from a full reload to a status delta. The customer success team stopped fielding the complaint. The rebuild is not scheduled, and the money went to the features customers had been asking for.
We report the 95th percentile on purpose. The median hides the worst experience, and the worst experience belonged to the largest accounts, the ones most likely to leave. The status delta also cut server load on every refresh, since the dashboard now sends only shipments whose status changed instead of the full list every minute.
What we would do differently
Load-test the new server-side filters before launch, not after. One combination of filters on the largest account produced a slow query in the first week that the planner had not shown on the test data. It was fixed in an afternoon, but it should have been found in sprint four.
The lesson is that a copy of the data is not the same as a copy of the usage. Next time we would replay a sample of real filter combinations from production logs during load testing, not just the default view.
If this sounds familiar
Slow pages are usually three or four specific problems, not a framework. The first week of any rescue engagement with us is a read-only audit that names them and prices each as a fix or a rebuild. Request a free performance audit and an engineer will start with your slowest page.
Frequently asked questions
How did you measure the improvement?
Real-user timing at the 95th percentile, before and after, for the same customer accounts. Lab numbers are useful for finding problems; field numbers are the result.
Would a rebuild have been faster eventually?
Not meaningfully. The same data and the same queries would have needed the same indexes and the same loading strategy. A rebuild buys a cleaner codebase, which this one did not need yet.
How much did it cost?
We do not publish client figures. Five weeks of two engineers at typical US agency rates will put you in the right range.
Where should web app performance optimization start?
With measurement, not code. Trace the slowest real requests, check the database queries behind them, and look at what the front end downloads before first render. In our experience, missing indexes and N+1 queries are the most common causes on data-heavy dashboards.
When is a rebuild the right call?
When the framework or runtime is no longer supported, when no one can safely change the code, or when the audit shows the slowness is structural rather than a short list of fixable problems. If the fixes add up to most of a rebuild's cost, rebuild.



