The marketing overview page in the admin tool had over 80 blocking SQL queries on a single load. I opened a ticket expecting to find one bad query to tune.

That instinct was wrong. The page had no single slow query. It had a blocking posture nobody had ever named as the problem, so nobody had ever fixed it.

What the page was actually doing

The production app is an old ASP Classic application. The marketing overview exists for internal ops, a dashboard of product performance, session counts, and summary metrics across the customer base. It looked normal, and had always looked normal. It was just slow.

When I traced the execution path, the page was built on an implicit contract: the server would finish all its work before the browser got anything. Every panel queued behind the one before it. SQL ran serially, one recordset closed before the next one opened. The ribbon section, the product table, the session counts, the PPC numbers, all of it waited in line. The page didn’t have a single slow query. The problem was the blocking posture itself.

That number didn’t happen by design; it accumulated. One quarter someone adds a product breakdown. The next year someone adds session stats. Nobody asks how the page performs with each addition, because the page still loads. Slowly, but it loads, and everyone gets used to waiting. The slowness becomes its personality.

The wrong diagnosis

The first instinct on a slow page is to find the offending query: a missing index, a cross join, a subquery that should be a join. Fix the worst one, rerun, measure. I’ve followed this instinct on other pages in this codebase, and it’s often right.

It would have been the wrong move here. The slowness wasn’t concentrated in one place; it was distributed across a contract nobody had challenged. Even a substantial improvement to one query would have left the page’s blocking structure largely intact. The architecture, not a single query, was the larger problem.

Ribbon first, everything else later

The real fix was to throw out the page contract.

I kept only the ribbon data in the initial server response. The ribbon is the summary strip visible first. The product panels, session counts, and auxiliary metrics loaded afterward through independently requested JSON endpoints, with the client responsible for keeping the work bounded and recoverable.

On the client side, I rebuilt the section loader so each section could render as its data arrived. The initial response contained the ribbon, while the remaining sections filled in on their own schedules. A slow follow-up section no longer blocked the initial response or every other section.

The initial request now has a narrower responsibility than the old all-at-once load path.

Deletion with receipts

Performance work on legacy code often includes deletion. Alongside asking “how do I make this faster,” ask whether the work is still load-bearing at all.

Two things were not load-bearing.

The PPC computation was server-side work that ran on every load and fed data into a section that, when I actually looked, was not wired up to display anything. The computation was real, but the consumer had been gone for who knows how long. I cut it.

The session count queries were running twice: once in the ribbon pass and once in the main body. Same query, same parameters, same recordset, two trips to SQL. The second one was a copy that had drifted out of sync with where the data was actually used. I kept one and removed the other.

Neither finding was necessarily the dominant bottleneck. But dead work is still work, and these two cases showed how an accumulated page can retain computations and duplicate queries after their original purpose has changed.

One question I didn’t fully answer: how many other admin pages in the app are in the same shape. The session reports, billing summaries, and reporting views all load. I haven’t traced their query counts, and the marketing overview felt normal before I traced it too. The only way to tell the difference is to count.

The ribbon styling fix was the useful proof

After the architecture change went in, a small thing happened: I needed to fix a styling issue on the ribbon. A spacing adjustment, something cosmetic.

On the old page, a change to the ribbon would have required touching the same server-side template that controlled everything else. Any edit was a risk to the whole load path. I’d have read the surrounding code more carefully than the fix warranted, just to make sure I wasn’t breaking something downstream.

On the new page, the ribbon is isolated. I opened the relevant section, made the adjustment, confirmed it in the browser, and the rest of the page was not a concern.

That styling change was not a performance benchmark. It was a maintainability observation: the ribbon had become a smaller, isolated surface, so a local presentation fix no longer required reasoning about the entire page-load path.