The WordPress Performance Profiling Toolkit We Actually Use

Search "code profiler wordpress" and you get two kinds of results: a plugin listicle, or a vendor’s landing page for their APM product. Neither tells you which tool to reach for when a specific symptom shows up, because neither one has actually been in the room when something broke. This is the toolkit that has — four tools, each one earned its place in a real investigation on a WordPress VIP-style multisite network of regional news sites, and each one is useless for at least one class of problem the others catch easily.

No tool here is exotic. That’s the point. The skill isn’t owning a profiler; it’s knowing which one is blind to the bug you actually have.

TL;DR: Four tools, each earned its place in a real investigation on a WordPress VIP-style multisite network, each blind to at least one class of problem the others catch easily: SAVEQUERIES for counting queries, EXPLAIN ANALYZE for why one query is slow, Chrome’s Performance panel for interaction responsiveness, and reading the trace itself for confirming a tool is measuring what you think it’s measuring.

Which Tool Answers "Which Query, and How Many Times"? SAVEQUERIES, Opt-In

WordPress ships most of a query profiler already. Define SAVEQUERIES and every query for the current request lands in $wpdb->queries with timing attached. The problem with using it as documented — as a wp-config.php constant — is that it’s now on for every request, forever, which is overhead nobody wants running in production permanently.

The fix is that wpdb checks SAVEQUERIES live, per query, not once at boot. That means it can be flipped on from a gated mu-plugin instead, for exactly the duration of a debugging session, and still capture the whole request — REST calls, admin-ajax, WP-CLI, not just page loads. The full writeup, including the mu-plugin is its own post; the short version is that it turned a "the sitemap is slow, sometimes" bug report into a hard number — nine to eleven seconds, every single time, on one specific endpoint a production slow-query log sample had never happened to catch.

What it’s for: counting queries and their durations across a full request, including endpoints that don’t get enough traffic to show up in a rolling production log.
What it’s blind to: why a specific query is slow. It’ll tell you a query took 9 seconds. It won’t tell you it’s because MySQL is ignoring your index.

Which Tool Answers "Why Is This One Query Slow"? EXPLAIN ANALYZE

Once SAVEQUERIES points at a specific slow query, the next question — why — has exactly one reliable answer: ask the database directly.

EXPLAIN ANALYZE
SELECT DISTINCT post_modified_gmt
FROM wp_posts USE INDEX (`type_status_date`)
WHERE post_type = 'post'
  AND post_status = 'publish'
ORDER BY post_modified_gmt DESC
LIMIT 1000;

This is the tool that found a hardcoded index hint, buried in a private method of a third-party SEO plugin, that was silently sorting close to 100,000 rows in memory on every sitemap request — because the index it was using didn’t actually match the column being sorted on. A raw timing number would never have shown that. The EXPLAIN ANALYZE output did, immediately: an index scan followed by an explicit Sort step whose cost scaled with the full row count instead of the LIMIT. That mismatch is the signature to look for every time.

What it’s for: any single query you already know is slow, when you need to know whether it’s a missing index, a mismatched one, or something else in the plan.
What it’s blind to: anything above the database. A slow page caused by JavaScript, by a misconfigured cache, or by a third-party script won’t show up here at all — the query itself can be perfectly healthy.

Which Tool Answers "Why Did This Interaction Feel Slow"? Chrome’s Performance Panel

Query-level tools have nothing to say about responsiveness — the gap between a click and the page visibly reacting. For that, Interaction to Next Paint and the browser’s own main-thread timeline are the only view that matters.

The move: open a representative page, record a session in the Performance panel, click something a real user would click, then read the timeline starting at the click, not after it. A long, unbroken block of script evaluation beginning exactly when the interaction happened is a specific, recognizable shape — and in one case it pointed straight at a performance-optimization plugin whose "defer non-critical JS until interaction" strategy had the perverse effect of scheduling 400KB of script execution for the exact moment INP gets measured. A plugin built to help was the regression.

What it’s for: anything where the complaint is "this feels slow to use," not "this page takes a long time to load."
What it’s blind to: what’s slow on the server. If the interaction is waiting on a slow API response rather than a long main-thread task, this tool will show you the wait, but the fix lives back in the query tools above.

Which Discipline Answers "The Number Won’t Move"? Reading the Trace, Not Just the Score

The strangest failure mode isn’t a slow tool, it’s a correct tool measuring the wrong thing. Largest Contentful Paint stayed bad on a content network for weeks of legitimate hero-image optimization — smaller files, better srcset, preload hints — because none of it touched what Lighthouse was actually flagging as the LCP element. Reading the trace’s selector path instead of skimming past it to the score revealed the real answer: a cookie-consent banner’s logo, rendering before the article content and outsizing it on screen.

What it’s for: confirming a tool is measuring what you think it’s measuring, before you spend another week optimizing the wrong element.
What it’s blind to: nothing, technically — it’s not really a separate tool, it’s a discipline. Read what the report says is happening, not just the headline number it gives you. Applies equally to the three tools above.

What Do None of These Tools Catch?

All four of these operate inside your own request/response cycle — a query, an interaction, a paint event. None of them will tell you that a cache layer is serving one visitor a broken page because of what a previous, unrelated request did to it, and none of them will tell you that your Search Console response-time numbers are skewed by a crawler you didn’t know was hitting your site disproportionately. Those live outside the request entirely — in logs and in aggregate reporting — and need a different kind of profiling.

And once a fix touches the schema rather than just a query, there’s a fifth discipline that isn’t really a tool at all: sequencing the deploy itself so the index exists before the code that assumes it does.

What’s the Generalizable Lesson Here?

  • Match the tool to the layer, not the symptom’s vibe. "Slow" can mean database, main thread, or a misattributed metric, and each one has a different tool with a different blind spot.
  • An opt-in profiler beats an always-on one for anything you’d feel bad leaving running in production — gate it, don’t delete it.
  • The database will tell you the truth about a query if you ask it directly (EXPLAIN ANALYZE); a timing number alone will not.
  • For responsiveness metrics, read the main-thread timeline starting at the interaction. The "what got scheduled to run on interaction" question is different from "what’s slow in general."
  • Before optimizing harder, confirm the report is measuring what you assume it’s measuring. Read the selector, not just the score.