All WordPress HTML Templates Forms & Webhooks AI & Tools
WordPress

Why Your WordPress Site Is Slow: A Diagnostic Method, Not a Plugin List

A practitioner’s diagnostic method for a slow WordPress site: split TTFB from render time, profile hooks, audit autoloaded options and bisect plugins

A real working moment illustrating the theme of an article about wordpress slow. Wide 16:9 banner, one strong focal point, magazine editorial quality, authentic and unstaged.

A client forwarded us a PageSpeed Insights screenshot last month. Score of 31, red everywhere, and one question: “which caching plugin should we buy?” We spent eleven minutes measuring instead of guessing. The answer was a single autoloaded option holding 2.8MB of serialised log data, written by a form plugin that had been “working fine” for three years. TTFB dropped from 1.9s to 240ms. No new plugins.

Nearly every “my WordPress site is slow” ticket arrives with a proposed solution attached. Install this, buy that, switch hosts. The proposals are almost always aimed at the wrong half of the timeline. If your server takes 1.4 seconds to emit the first byte, no amount of image lazy loading will save you. If your hero image is a 2.4MB uncompressed PNG, Redis will not help.

So here’s the method we actually use, in order, on real sites. It’s a decision tree, not a shopping list.

Key Takeaways

  • Split the page load into server time (TTFB) and browser time (LCP minus TTFB) before doing anything else. The two halves have completely different causes and completely different fixes.
  • Measure TTFB with curl -w against a cache-busting URL, five times, and take the median. A single PageSpeed run tells you almost nothing about server variance.
  • Bloated wp_options autoload is still the single most common cause of slow uncached WordPress responses. WordPress 6.6 added a 150KB threshold and new autoload values, but it doesn’t retroactively fix rows plugins already wrote.
  • Query Monitor plus wp profile hook will name the guilty plugin in about ten minutes. Guessing from a plugin list takes an afternoon and is usually wrong.
  • On the frontend, LCP is an image or a web font roughly nine times out of ten. Fix the element that is actually the LCP element, not the whole page.

Split the timeline before you touch anything

Open DevTools, Network tab, hard reload, and click the document request. Look at the Timing panel. You get a number for “Waiting for server response”. That’s your TTFB. Then look at your LCP element in the Performance panel or via the Web Vitals extension.

Now do the subtraction. If TTFB is 1,400ms and LCP is 2,600ms, you have a 1.4 second server problem and a 1.2 second browser problem, and the server problem gates everything else. If TTFB is 180ms and LCP is 4.1 seconds, your server is fine and your theme is the problem. These are entirely different investigations, and people mix them constantly.

Rough targets we hold ourselves to on client work: under 200ms TTFB for a cache hit, under 600ms for an uncached logged-out page, under 2.5s LCP on a 4G throttle. Google’s “good” threshold for TTFB is 800ms, which is generous. Treat it as a ceiling, not a goal.

Measure WordPress TTFB properly, twice

Browser DevTools measures your connection to the site, including your own latency to the edge. That’s useful, but it conflates network distance with server work. Use curl:

curl -o /dev/null -s -w "dns:%{timenamelookup} tcp:%{timeconnect} tls:%{timeappconnect} ttfb:%{timestarttransfer} total:%{time_total}\n" \
  "https://example.com/?cachebust=$RANDOM"

The cache-busting query string matters. Without it you’re measuring your page cache, which is the number you want to be fast, but not the number that diagnoses anything. Run it five times, look at the median, and compare it to the same URL without the query string.

  • Cached fast, uncached fast: the server is not your problem. Go to the frontend section.
  • Cached fast, uncached slow (over ~800ms): classic WordPress application problem. Your cache is hiding it from Google’s crawler on cold pages, and every logged-in user, cart page and search result eats the full cost.
  • Both slow: your page cache isn’t working at all, or you’re on genuinely underpowered hosting. Check response headers for x-cache, cf-cache-status or your host’s equivalent before blaming PHP.

Also check the TLS and connect numbers. We’ve seen a “slow WordPress” case that turned out to be 700ms of TLS handshake because the host was terminating in a different continent to the CDN edge. Nothing in WordPress could have fixed that.

If the server is slow: where the time actually goes

Autoloaded options

Every WordPress request loads all autoloaded rows from wp_options into memory in one query. On a healthy site that’s 200KB to 800KB. We regularly find sites at 5MB or more, because plugins write logs, license payloads and cached API responses as autoloaded options and never clean up.

SELECT optionname, ROUND(LENGTH(optionvalue)/1024, 1) AS kb
FROM wp_options
WHERE autoload IN ('yes','on','auto','auto-on')  -- 6.6 introduced the new values
ORDER BY LENGTH(option_value) DESC
LIMIT 20;

Or with WP-CLI, which is safer on a live box:

wp option list --autoload=on --fields=option_name,size --format=table --orderby=size --order=desc | head -25
wp transient delete --expired

WordPress 6.6 added a size threshold (options above roughly 150KB are no longer autoloaded by default when set through add_option) and split autoload into on, off, auto, auto-on and auto-off. Good change. It does nothing for the 3MB row a plugin wrote in 2022. You have to find it yourself.

Before you flip anything to off, check what writes it. If a plugin reads that option on every request, un-autoloading it just converts one big query into one big query plus a lookup. Delete orphaned rows from uninstalled plugins instead. And take a backup first, which you should have anyway (our 3-2-1 backup approach exists precisely for these afternoons).

The object cache

Without a persistent object cache, WordPress rebuilds the same term queries, meta lookups and option reads on every single request. Adding Redis or Memcached via a drop-in object-cache.php is the highest-leverage server-side change on most content-heavy sites, and we’ve seen it cut uncached TTFB by 40 to 60 percent on WooCommerce installs.

It is not free. A misconfigured object cache with no eviction policy will fill memory and start thrashing, and stale cache groups cause bugs that look like ghosts. Set maxmemory-policy allkeys-lru and actually monitor hit rate.

Cron and admin-ajax

Default WP-Cron fires on page loads. On a low-traffic site that means the unlucky visitor pays for your daily backup job. Disable the pseudo-cron and use a real one:

// wp-config.php
define( 'DISABLEWPCRON', true );
# crontab, every 5 minutes
/5    * cd /var/www/example.com && wp cron event run --due-now --quiet

Then run wp cron event list. If you see 40 overdue events, something has been failing silently for months.

Reading Query Monitor without lying to yourself

Install Query Monitor, load the slow page as an administrator, and open the Queries panel. Sort by time. What you’re looking for is not the total query count (300 queries at 0.2ms each is fine) but the shape of the distribution: one query at 900ms, or forty near-identical queries that scream missing cache.

Use the “Queries by Component” tab. It attributes SQL time to the plugin or theme that caused it, which cuts through the guessing immediately. A related trap: heavy metaquery on unindexed postmeta. If a filtered archive is doing three joins across wppostmeta on a 40,000 post site, no cache layer will make that acceptable. The correct fix is a data model change, not a plugin. We wrote about the storage side of that in the custom fields guide.

For hook-level attribution, WP-CLI’s profile command is better than anything in the browser:

wp package install wp-cli/profile-command:@stable
wp profile stage --url=https://example.com/ --fields=stage,time,cache_ratio
wp profile hook --all --url=https://example.com/ --spotlight --orderby=time --order=desc

--spotlight hides everything that took a trivial amount of time. Usually two or three hooks account for most of the request, and one plugin owns them.

If the browser is slow: find the LCP element first

Stop optimising “the page”. Identify the single element the browser considers the Largest Contentful Paint, then optimise that element and the resources blocking it. In Chrome’s Performance panel, the LCP marker is clickable and tells you exactly which node it is.

In practice it’s one of four things:

  1. A hero image that isn’t preloaded and is lazy loaded. Never set loading="lazy" on your LCP image. WordPress core tries to avoid this now, but page builders override it constantly. Add fetchpriority="high" to the hero image.
  2. An image served at 2400px into a 720px slot as JPEG, with no AVIF or WebP fallback chain. Our image optimisation guide covers the sizing maths.
  3. Web fonts blocking text paint. Self-host, preload the one weight you actually need above the fold, and set font-display: swap.
  4. A slider or hero animation that waits on JavaScript before rendering anything. This is the expensive one, because the fix is usually a redesign of that block.

Separately, watch INP. Since it replaced FID in March 2024, the sites that quietly fail Core Web Vitals are typically the ones running four analytics tags and a chat widget on the main thread. The 200ms threshold is not forgiving. Defer third-party scripts, and delete the two nobody looks at.

Bisect to prove it, don’t guess from the plugin list

Once you have a suspect, prove it. Use the Health Check & Troubleshooting plugin’s troubleshooting mode, which disables plugins for your session only, so the live site stays up for everyone else. Disable half. Measure. Disable half of the remaining half. Measure. Six steps gets you through 60 plugins.

Two things people get wrong here. First, measure the same URL with the same cache-bust each time, five runs, median. Single measurements on shared hosting have enough variance to send you down a completely wrong path. Second, a plugin can be slow only in combination with another one (a SEO plugin filtering queries that a builder then runs 40 times), so re-enable in the same order you disabled.

Record the numbers in a text file as you go. Ten minutes later you will not remember whether step four was 610ms or 910ms.

When your WordPress site is slow, fix in this order

Priority is not preference. It’s cost per millisecond saved:

  1. PHP version and OPcache. If you’re still on 7.4, moving to 8.2 or later is typically a 20 to 30 percent reduction in PHP execution time for free. Check OPcache is on and not hitting its memory limit.
  2. Autoloaded options and orphaned data. Free, fast, often dramatic.
  3. Persistent object cache. One-time setup, permanent benefit.
  4. Full page cache at the edge with correct bypass rules for cart, checkout and logged-in users.
  5. The LCP element and render-blocking CSS.
  6. Removing the plugin bisecting identified or replacing it with 30 lines in a child theme.

Where this advice doesn’t apply: a genuinely bad host. If you’re on a $3/month shared plan with a CPU quota you hit at 40 concurrent visitors, everything above buys you maybe 300ms and then you plateau. Migrate. Similarly, if the theme itself queries the database inside a template loop, you’re not tuning your way out. We rebuilt one client’s category template from a bloated multipurpose theme to a clean Bootstrap 5 build on CanvasWP and the uncached TTFB went from 1.6s to 310ms, mostly by deleting features nobody used.

Frequently Asked Questions

What is a good TTFB for WordPress in 2026?

Aim for under 200ms on a cached page and under 600ms uncached, measured from a location near your host. Google’s “good” bucket is 800ms, but that’s the pass mark, not a target. If your uncached TTFB is over one second, you have an application problem, not a hosting-tier problem.

Does having a lot of plugins make WordPress slow?

Plugin count is a weak predictor. We’ve seen 62-plugin sites at 240ms TTFB and 9-plugin sites at 2.1 seconds. What matters is what each plugin does per request: uncached HTTP calls, unindexed meta queries, and autoloaded option writes. Profile with wp profile hook instead of counting.

Which caching plugin should I install first?

None, until you’ve measured. If your host already provides server-level page caching (most managed WordPress hosts do), adding a caching plugin on top frequently causes double-caching bugs and stale carts. Add a persistent object cache before a second page cache layer.

Why is my PageSpeed score bad when the site feels fast?

PageSpeed Insights runs a throttled mobile lab test that is deliberately pessimistic, and the score is a weighted composite you can’t debug directly. Look at the field data section (real Chrome user data) for LCP, INP and CLS instead. If field data is green and the lab score is 45, ignore the score.

Will switching to a lighter theme fix a slow site?

Only if profiling shows theme code in the hot path, which it does maybe a third of the time. A theme that adds 30 database queries per page and loads six CSS files is worth replacing. Migrating themes is expensive and risky, so confirm with Query Monitor’s “Queries by Component” view before committing to it. Our performance playbook covers the full measurement chain.

Do this next: run the curl command above five times against a cache-busted URL and write down the median. That single number decides whether you spend your afternoon in wp_options or in the Performance panel. Everything else is a guess with a plugin attached.

bisect plugins to find slow wordpress plugin how to diagnose wordpress speed problems query monitor slow query diagnosis wordpress lcp element optimisation wordpress slow wordpress ttfb too high fix wp profile hook wp-cli performance wp_options autoload bloat slow site