Latest
DDR5 prices dropped 18% this quarter — time to rebuild? WordPress 6.9 ships a new default caching layer Core Web Vitals adds INP thresholds for mobile
Read more
MacBook on a desk displaying source code next to a blue PHP mascot
WordPress

The Complete WordPress Speed Playbook: From 6 Seconds to Under One

A real client site. A real 6.2-second load. Eleven steps, documented in the order we did them — including the two that made things worse before they made them better, and the one everybody skips that turned out to matter most.

The site was a mid-sized publisher: about 4,200 posts, a WooCommerce store bolted on for merchandise, six years of accumulated plugins and three developers’ worth of inherited decisions. The brief we received said “we need a CDN”. The brief was wrong, which is normal. People arrive with a solution rather than a symptom because someone told them the solution on a podcast.

What follows is the actual sequence, with the measurement after each stage. Nothing here is theoretical; every number is from this site.

6.2 s → 0.94 smobile LCP, 75th percentile field data
1,847 → 68database queries per uncached page load
11steps, in the order they were actually done

The patient: 6.2 seconds and a bad diagnosis

Initial field data, taken from real users over the preceding 28 days:

75th percentile, all devices, 28-day rolling window. These are the numbers search engines see, not lab scores.
MetricMobileDesktopTarget
Largest Contentful Paint6.24 s3.81 s< 2.5 s
Interaction to Next Paint486 ms210 ms< 200 ms
Cumulative Layout Shift0.340.29< 0.1
Time to First Byte1.41 s1.38 s< 0.6 s

Note the TTFB. It is 1.41 seconds before a single byte of HTML arrives, which means no amount of front-end work can get LCP under 2.5 s. A CDN would have cached static assets and left that 1.41 s almost untouched. This is why the order of operations matters more than the list of techniques.

Order of operations

Front-end optimisation on a slow server is painting a house with wet walls. Until TTFB is under about 600 ms there is a hard floor on LCP that no amount of image compression will get you under. Server first, always.

Step 1 — Measure before you touch anything

Three things get recorded before any change, and all three go in a document with a date on it:

  • Field data from real users, segmented by device and by page template. Site-wide averages hide the fact that your product pages are fine and your category pages are catastrophic.
  • A server-side trace of one uncached page load: total PHP time, number of database queries, slowest query, peak memory.
  • A waterfall from a throttled connection, because everything looks fast on office fibre.

Our trace of the homepage: 1,847 database queries, 1.19 s of PHP execution, 214 MB peak memory. For context, a healthy WordPress page is 30–80 queries. Eighteen hundred is a defect, not a tuning opportunity.

Step 2 — Fix the server before the front end

Three server-level changes, in ascending order of effort:

PHP version

The site ran PHP 7.4, which had been unsupported for some time. Moving to PHP 8.4 required fixing four deprecation warnings in the theme and replacing one abandoned plugin. The payoff was immediate: PHP execution time fell 41%, from 1.19 s to 0.70 s, with no other change. This is the single highest return-per-hour action available on most legacy WordPress sites.

OPcache configured, not merely enabled

OPcache was on with default settings, which meant it was evicting compiled scripts constantly because the memory allocation was too small for a site with this many plugins. Raising opcache.memory_consumption to 256 MB and opcache.max_accelerated_files to 20,000 removed the thrashing. Another 120 ms.

Hosting that is not oversold

The site was on shared hosting whose TTFB varied from 400 ms to 2.8 s depending on the time of day — a classic noisy-neighbour signature. Moving to a container with guaranteed CPU allocation cost an extra £22 a month and stabilised TTFB more than any software change we made.

The cheapest win on the internet

Upgrading PHP on a legacy WordPress site returns more performance per hour of work than anything else we do. If you are on PHP 7.x in 2026 you are leaving roughly 40% of your server performance unused — and running unsupported software.

After step 2: TTFB 1.41 s → 0.62 s. LCP 6.24 s → 4.41 s. No front-end work yet.

Step 3 — Find the slow queries

1,847 queries needed explaining. We enabled the MySQL slow query log with a 50 ms threshold and ran a profiling plugin on staging. Three findings, which between them accounted for most of it:

  1. An unindexed meta_key lookup in a “related products” feature, executing a full table scan of a 2.1-million-row postmeta table on every page load. One composite index: 340 ms saved per request.
  2. A plugin querying inside a loop — one query per post to fetch a custom field, 24 posts per page, on three widgets. Replaced with a single primed meta cache before the loop: 71 queries removed.
  3. Autoloaded options bloat. The wp_options table held 3.4 MB of autoloaded data, much of it transients left behind by plugins that had been deleted years earlier. Every single request loaded all of it.
-- Find your autoload offenders
SELECT option_name, LENGTH(option_value) AS size_bytes
FROM wp_options
WHERE autoload = 'yes'
ORDER BY size_bytes DESC
LIMIT 25;

Anything above ~100 KB in that list deserves investigation. Anything belonging to a plugin you no longer have installed can go.

Run this query on your own site

The autoloaded-options query above takes two seconds and reveals problems on roughly half the sites we look at. Sites that have been running for more than three years almost always have megabytes of orphaned transients loading on every single request.

After step 3: 1,847 queries → 94 queries. PHP time 0.70 s → 0.31 s.

A performance dashboard showing response-time and throughput charts
Object-cache hit rate is the number to watch after deployment. Below 90 per cent means memory or an over-eager cache flush, not bad luck.

Step 4 — Object caching done properly

WordPress caches objects in memory for the duration of a single request and then throws them away. A persistent object cache — Redis or Memcached — keeps them between requests, which is transformative for logged-in users and for any page that cannot be page-cached.

This is the step that went wrong first. Our initial Redis deployment used a single database shared with another application, and key collisions produced occasional stale output that took two days to diagnose. Lessons that are now in our runbook:

  • Give each site its own Redis database index and a unique key prefix.
  • Set maxmemory-policy to allkeys-lru. The default will refuse writes when full rather than evicting.
  • Allocate real memory. Our 4,200-post site needed 512 MB; the initial 128 MB caused constant eviction and a cache hit rate of 61%.
  • Monitor the hit rate. Below 90% means something is wrong — usually memory or a plugin flushing the whole cache on every save.

Once configured correctly: cache hit rate 97.2%, uncached page generation 0.31 s → 0.14 s, and logged-in admin pages went from painful to instant.

Step 5 — Page caching and the rules that break it

Full-page caching is the largest single win available and the easiest to get subtly wrong. The mechanism is simple: store the finished HTML, serve it to the next visitor without running PHP at all. The difficulty is entirely in the exclusions.

Must never be cachedWhy
Cart, checkout, account pagesPer-user content; caching leaks one customer's data to another
Logged-in sessionsPersonalised menus, admin bar, draft content
Requests with a cart cookie setStale basket totals are worse than a slow page
Search result pagesUnbounded cache-key space; fills the cache with single-use entries
Anything with a nonce in the HTMLCached nonces expire and silently break forms

We also set a sane invalidation policy rather than the common “purge everything on any change”, which on a busy site means the cache is perpetually cold. Publishing a post now purges that post, its archives, the homepage and the sitemap — not the other 4,199 pages.

Caching a cart page

The most serious production incident we have seen in WordPress performance work was a page cache configured without a cart-cookie exclusion. Customers saw each other's basket contents, including names and partial addresses. Test your exclusions on staging with two real sessions before you go live. Every time.

After step 5: cached TTFB 0.62 s → 0.11 s. LCP 4.41 s → 2.92 s.

Step 6 — The plugin audit

The site had 47 active plugins. We measured each one's cost by deactivating it on a staging clone and re-running a ten-request trace. The results were, as usual, extremely uneven:

Mean added time per uncached request across ten runs on staging.
Plugin categoryCountCost per requestAction
Page builder + addons4410 msKept — rebuild scheduled separately
Social sharing (two competing plugins)2180 msReplaced with 30 lines of theme code
Related posts (database-driven)1290 msReplaced with a cached taxonomy query
Analytics wrappers395 msConsolidated into one
Abandoned / no longer updated6140 msRemoved entirely
Slider plugin used on one page175 ms site-wideConditionally loaded

Total removed: eleven plugins and roughly 620 ms per uncached request. Note the slider — a plugin used on exactly one page was loading its CSS and JavaScript on all 4,200. Conditional loading is often more valuable than removal, because it keeps the feature.

How to run a plugin audit

Clone to staging. Record ten uncached loads with a profiler. Deactivate one plugin. Record ten more. Reactivate. Repeat. It takes an afternoon for forty plugins and it replaces every argument about which plugin is “heavy” with a number.

Step 7 — CSS: the render-blocking problem

The site shipped 412 KB of CSS across 14 separate stylesheets, every one of them render-blocking. The browser cannot paint until it has all of them.

What worked, in order of impact:

  1. Inline the critical CSS for above-the-fold content (about 9 KB) directly in the <head>, and load the rest asynchronously. LCP improved by 640 ms on mobile immediately.
  2. Remove unused rules. A coverage analysis showed 71% of the CSS was never applied on a typical article page — mostly page-builder widget styles for widgets this template never uses.
  3. Stop combining files blindly. The previous developer had merged all 14 into one 412 KB file to “reduce requests”. Under HTTP/2 that advice is a decade out of date, and it meant every page downloaded every rule. Splitting by template beat combining.
Automatic critical CSS needs supervision

Automated critical-path tools generate their CSS from one snapshot of one viewport. Change the template and the inlined CSS silently goes stale, producing a flash of unstyled content that only some visitors see. Regenerate on deploy, and check the output on a real phone.

Step 8 — JavaScript: load order over file size

JavaScript hurts differently from CSS. It does not merely delay paint; it occupies the main thread, and an occupied main thread cannot respond to taps. That is what INP measures, and INP is where this site was worst.

Three changes, no file-size reduction involved:

  • defer on everything non-critical. Not asyncdefer preserves execution order, which matters when scripts have dependencies. Nineteen scripts moved.
  • Break up long tasks. One analytics initialiser ran a 340 ms synchronous block on load. Yielding to the main thread between chunks took the longest task to 48 ms.
  • Load on interaction. The comment system, the share widget and the search autocomplete now load when the user shows intent, not on page load. Together they were 180 KB of JavaScript that most visitors never used.

INP: 486 ms → 174 ms. The single biggest contributor was breaking up that one long task — a 4 KB change in a 900 KB bundle.

A desk with a monitor, keyboard and a small mascot figure
Images were 68 per cent of page weight before the rebuild — a proportion that has held on almost every site we have audited.

Step 9 — The image pipeline

Images were 68% of page weight. They usually are.

ChangeDetailEffect
Modern formatsAVIF with WebP and JPEG fallbacks−54% image bytes
Correct dimensionsStop serving 2400 px files into 720 px slots−31% on top of the above
Lazy loading below the foldloading="lazy", but never on the LCP image−1.1 s to first paint
Explicit width and heightOn every single image tagCLS 0.34 → 0.04
Preload the LCP imageWith fetchpriority="high"−380 ms LCP
Never lazy-load the LCP image

It is the most common self-inflicted performance wound we see. A blanket lazy-load applied to every image delays the one image the metric is actually measuring, and typically adds 800–1,500 ms to LCP. Exclude the hero image explicitly, and give it fetchpriority="high".

Step 10 — Fonts, the quiet LCP killer

The site loaded five font families in nine weights from a third-party service. Each one required a DNS lookup, a TLS handshake and a request chain before any text could render in its intended face.

What we did, and would do again on any site:

  1. Self-host. Removes an entire third-party connection from the critical path. Worth 200–400 ms on a cold mobile connection, every time.
  2. Subset aggressively. The site is English-only; the fonts shipped Cyrillic, Greek and Vietnamese glyphs. Subsetting cut each file by roughly 70%.
  3. Cut to two families, four weights. Nobody noticed. Nobody ever notices.
  4. font-display: swap so text is visible immediately in a fallback face.
  5. Preload only the two faces used above the fold. Preloading all of them is the same as preloading none.

−510 ms LCP on mobile. This step is routinely skipped and routinely one of the three biggest wins available.

Step 11 — Third parties and the honesty conversation

Remaining on the page: two analytics platforms, a heat-mapping tool, a chat widget, an advertising script and two social embeds. Together they accounted for 1.4 s of main-thread time and a 620 ms contribution to LCP on mobile.

This is not a technical problem. It is a conversation with the people who added each tag, and the only useful framing we have found is: what decision did this tool inform in the last quarter, and what would we lose if it were gone tomorrow?

Outcome on this site: heat-mapping removed (unused for fourteen months), one of the two analytics platforms removed (duplicate data), chat widget deferred until user interaction, social embeds replaced with static images linking out. The advertising script stayed, because it pays for the site — but it moved to an asynchronous, sandboxed load.

The third-party test

Before adding any tag, ask what decision it will inform and who will look at it. Before keeping one, check when someone last opened its dashboard. On this site, two of six failed that test immediately and nobody missed them.

Final numbers and what actually mattered

Field data, 75th percentile, mobile, measured 28 days after the final deployment.
MetricBeforeAfterChange
Largest Contentful Paint6.24 s0.94 s−85%
Interaction to Next Paint486 ms112 ms−77%
Cumulative Layout Shift0.340.02−94%
Time to First Byte1.41 s0.11 s−92%
Page weight4.8 MB0.9 MB−81%
Database queries (uncached)1,84768−96%

Ranked by contribution to the final LCP figure, the eleven steps landed like this:

  1. Page caching — the single largest win, once the server could generate a page quickly enough to cache.
  2. Server and PHP version — unglamorous, cheap, and a prerequisite for everything after it.
  3. Query and index work — invisible from the front end and worth more than every asset optimisation combined.
  4. Fonts — the step people skip.
  5. Critical CSS — large gain, moderate ongoing maintenance cost.
  6. Images — biggest byte reduction, smaller time reduction than the byte count suggests.
Nobody arrives asking for a database index. They arrive asking for a CDN, because a CDN is a thing you can buy. The work that matters is usually the work nobody sells. Priya Raghunathan, Web Performance Editor

Two final notes. First, the CDN the client originally asked for was eventually added — as step twelve, worth about 90 ms. It was the right tool for the wrong problem. Second, none of this holds without maintenance: six months later we found the plugin count back up to 34 and the autoloaded options creeping past 1 MB. Performance is a habit, not a project.

If you want this done on your site, our WordPress service runs exactly this sequence, and the technical audit produces the prioritised backlog it starts from. The companion piece to this article is our Core Web Vitals guide, which explains how the metrics above are actually collected.

Questions readers keep asking

Less important than how it is configured. Any of the mainstream page-cache plugins will get you most of the way if the exclusions are right and invalidation is sensible. What matters far more is whether you also have a persistent object cache, whether your host does page caching at the server level (faster than any plugin), and whether your PHP version is current. We have seen a well-configured free plugin comfortably beat a badly configured premium one.

If you have meaningful traffic from outside your server's region, yes — physics does not negotiate. If your audience is local and your TTFB is already 110 ms, a CDN is worth perhaps 50–100 ms and is not where your next hour of work should go. Fix the server, the queries and the fonts first; add the CDN when it is the largest remaining item.

Not always, but it is always a cost. On this site the builder added around 410 ms per uncached request and a great deal of unused CSS. That can be acceptable if it is what lets a non-technical team publish without a developer. The failure mode is using a builder for pages that are structurally identical — a custom block or template is faster to render, faster to load and easier to change consistently.

Monthly for field data, and on every deployment for the lab checks. Sites do not stay fast; they drift. Six months after this project the plugin count was climbing again and autoloaded options had crept past a megabyte. A five-minute monthly check catches that while it is still five minutes of work.

Directly, a little — page experience is a real but modest ranking input, and it matters most as a tie-breaker between comparable results. Indirectly, a great deal: faster pages get crawled more efficiently, keep more visitors, and convert better. On this site organic traffic rose 22% over the following quarter, and we would attribute more of that to visitors staying than to any ranking change.

Priya Raghunathan
Web Performance Editor

Priya Raghunathan

Priya audits WordPress and headless sites for a living. She has taken more than three hundred sites through Core Web Vitals remediation and still keeps a folder of the worst plugin code she has ever seen.

Keep reading

Related guides