I’ve spent years watching WordPress sites slow down, speed up, and slow back down again after someone installs three more plugins. At SeedProd, where we’re pushing a page builder to over a million active installs, performance isn’t a thought I have once a quarter — it’s something I’m dealing with constantly. This post is the distillation of what I’ve actually found to move the needle, and just as importantly, what I’ve stopped wasting time on.
This isn’t a “install WP Rocket and call it a day” post. There’s nothing wrong with WP Rocket, but if you don’t understand why certain things work, you’ll keep fighting the same fires.
Start With a Real Diagnosis, Not Guesswork
Before touching a single setting, install Query Monitor. It’s free, it’s thorough, and it will tell you things PageSpeed Insights never will. The panels I go straight to:
- Queries — sorted by time. Anything over 0.05s deserves a hard look.
- HTTP API Calls — slow external requests that block PHP execution are one of the most common hidden killers.
- Hooks & Actions — to see what’s running on every request.
- Scripts & Styles — to audit what’s being enqueued and why.
On one client site last year, Query Monitor revealed a WooCommerce extension making 11 external API calls on every single page load, including static pages that had nothing to do with the store. Total added latency: ~1.4 seconds per request. Nothing in PageSpeed caught it because PageSpeed measures what arrives in the browser, not what happens on the server before the first byte.
Object Caching: The Highest-Leverage Change You Can Make
WordPress has a built-in object cache, but by default it only persists for the lifetime of a single PHP request. That means on the next page load, WordPress is re-running the same database queries all over again. Adding a persistent object cache backend — Redis or Memcached — is usually the single highest-leverage performance change you can make on a dynamic WordPress site.
Redis is my preference. Most managed hosts (Kinsta, WP Engine, Cloudways, GridPane) offer it as a one-click add-on. Once the server has Redis running, you drop in the wp-redis or object-cache-pro drop-in and add this to wp-config.php:
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_DATABASE', 0 );
define( 'WP_REDIS_TIMEOUT', 1 );
define( 'WP_REDIS_READ_TIMEOUT', 1 );
On a mid-traffic WooCommerce site — about 8,000 sessions a day — enabling Redis dropped average server response time from ~620ms to ~180ms. That’s not a typo. The database was fine; it just kept answering the same questions over and over. Give WordPress a memory, and it stops repeating itself.
Page Caching: Serving HTML Without PHP
Object caching helps dynamic page generation. Page caching eliminates it entirely for anonymous visitors by serving pre-generated static HTML. For most sites, logged-out users should never trigger a PHP execution at all.
A few non-obvious failure modes:
- Cache exclusions matter more than the cache itself. Cart pages, checkout, account pages, and any URL with query parameters tied to user state need to be excluded or you’ll serve one user’s cart to another person. I’ve seen this happen.
- Cache warmers are underused. A fresh cache after a purge means your first real visitor hits PHP. Tools like Screaming Frog or a simple cURL script run on a cron can pre-warm after deployments.
- Nginx FastCGI cache at the server level beats plugin-level page caching every time for raw speed. If your host or stack supports it, use it. The plugin approach is fine but adds overhead.
Database Query Optimization
This is where I spend a disproportionate amount of time, and it pays off every time.
Hunting Slow Queries
In Query Monitor’s Queries panel, enable slow query logging in MySQL (anything over 1 second to start, then lower the threshold as you clean things up):
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
The most common culprits I find are post meta queries without proper indexing, wp_options autoload abuse, and WooCommerce order queries scanning full tables.
Autoloaded Options: The Silent Killer
Every page load, WordPress pulls all autoloaded options from wp_options into memory in a single query. This is fine when the payload is reasonable. It becomes a problem when plugins store megabytes of serialized data in there with autoload = yes.
Check your autoload payload size:
SELECT
COUNT(*) as count,
SUM(LENGTH(option_value)) as total_bytes,
ROUND(SUM(LENGTH(option_value)) / 1024 / 1024, 2) as total_mb
FROM wp_options
WHERE autoload = 'yes';
Anything over 1MB is worth auditing. I’ve found sites sitting at 8-12MB of autoloaded data, most of it from deactivated (but not properly uninstalled) plugins. Find the offenders:
SELECT option_name, LENGTH(option_value) as size
FROM wp_options
WHERE autoload = 'yes'
ORDER BY size DESC
LIMIT 20;
For options that don’t need to be autoloaded, update them:
UPDATE wp_options SET autoload = 'no'
WHERE option_name = 'some_plugin_huge_option';
Transients Cleanup
Without a persistent object cache, transients live in wp_options. Expired transients pile up and never get cleaned unless something touches them. Run this periodically or add it to a WP-Cron job:
DELETE FROM wp_options
WHERE option_name LIKE '_transient_%'
AND option_name NOT IN (
SELECT CONCAT('_transient_', option_name)
FROM wp_options
WHERE option_name LIKE '_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP()
);
-- Simpler version that just nukes all expired ones:
DELETE o FROM wp_options o
JOIN wp_options t ON t.option_name = REPLACE(o.option_name, '_transient_', '_transient_timeout_')
WHERE o.option_name LIKE '_transient_%'
AND t.option_value < UNIX_TIMESTAMP();
Note: once you have Redis running, transients are stored there instead, so this becomes a non-issue.
Image Optimization: Do It Right Once
Images are still the largest assets on most pages, and the gains from getting this right are real.
WebP conversion is table stakes now. WordPress 5.8+ generates WebP versions of uploads natively, but you need a host and server configuration that serves them. Cloudflare’s Polish feature handles this transparently at the CDN layer if you’d rather not think about it. I typically serve 30-45% smaller image files after converting JPEG/PNG to WebP — on image-heavy pages that’s a dramatic reduction in total page weight.
Lazy loading is automatic in modern WordPress via the loading="lazy" attribute on <img> tags. Don’t disable it. The one exception: your above-the-fold hero image should have loading="eager" and an explicit fetchpriority="high" attribute, otherwise you’re intentionally delaying your LCP element.
<img
src="hero.webp"
srcset="hero-480.webp 480w, hero-800.webp 800w, hero-1200.webp 1200w"
sizes="(max-width: 600px) 480px, (max-width: 1000px) 800px, 1200px"
loading="eager"
fetchpriority="high"
alt="..."
>
Proper srcset matters more than most people realize. WordPress generates srcset automatically for registered image sizes, but if your theme or page builder is outputting images outside of wp_get_attachment_image(), you’re likely serving full-size images to mobile users. Audit this in your browser’s network tab filtered to images — check the “Size” vs “Transferred” columns.
Critical CSS and Render-Blocking Resources
Render-blocking CSS and JS delay the browser from painting anything. The fix has two parts.
For CSS: extract the styles needed to render above-the-fold content and inline them in <head>, then load the full stylesheet asynchronously. Tools like Critical or Penthouse can generate this. Manually curating it is tedious but more accurate for complex themes.
For JavaScript: almost everything should be defered. Very few scripts genuinely need to block rendering. In WordPress, you can add defer attributes via wp_script_add_data():
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_script( 'my-script', get_template_directory_uri() . '/js/main.js', [], '1.0', true );
wp_script_add_data( 'my-script', 'defer', true );
});
Third-party scripts — analytics, chat widgets, social share buttons — are the real offenders. I use Partytown via a simple integration to move Google Tag Manager off the main thread entirely. On one project this alone improved TBT (Total Blocking Time) by over 400ms in lab testing.
Plugin Audit Methodology
My process:
- Take a baseline TTFB and page load measurement with Query Monitor active.
- Deactivate plugins in batches of 3-4 (keep Query Monitor active).
- Re-measure after each batch. When you find the batch that causes a significant drop, re-activate them one at a time.
- Once the offender is identified, look at why it’s slow — Query Monitor’s Queries and HTTP API panels will tell you. Sometimes there’s a config fix; sometimes you need an alternative plugin.
Common patterns I find: plugins running expensive queries on every page even when their features aren’t being used, plugins making synchronous external HTTP requests during page generation, and plugins registering dozens of post types and taxonomies they never actually need.
CDN Configuration
I’ve used Cloudflare on nearly every site I’ve built in the last six years, and at this point I treat it as mandatory infrastructure rather than an optional optimization. The free tier gets you most of the way there: global edge caching, Brotli compression, HTTP/3, and basic DDoS mitigation.
The setting most people miss: under Caching, set your Browser Cache TTL to something sensible (I use 1 year for versioned assets) and configure Cache Rules to actually cache your WordPress pages at the edge for anonymous users. The default Cloudflare setup proxies traffic but doesn’t cache HTML — you have to explicitly tell it to. With proper edge caching configured on Cloudflare, I’ve seen TTFB drop from ~300ms to under 30ms for cached pages. The origin server becomes almost irrelevant for logged-out traffic.
PHP Version: This One’s Easy, Just Do It
If you’re running PHP 7.4 or earlier, upgrade to PHP 8.2 or 8.3. I know there are compatibility concerns, but the performance difference is real — PHP 8.x with OPcache properly configured is measurably faster than 7.4 on the same hardware. I’ve benchmarked 15-25% throughput improvements on identical WordPress codebases just from the PHP version bump. Test on staging, check your plugins’ compatibility in the PHP compatibility checker, fix any issues, and deploy. It’s worth the afternoon it takes.
Hosting Tier Reality Check
You cannot optimize your way out of bad hosting. I’ve seen sites on shared hosting with WP Rocket, Redis, CDN, and WebP images still delivering 3-second TTFB because the server is undersized and the network is congested. At some point, the bottleneck is just iron.
The tiers that actually perform for WordPress, in my experience: managed WordPress hosts (Kinsta, WP Engine, Pressable) for business-critical sites, Cloudways for value-for-money VPS management, and GridPane for agencies running their own servers. Anything on shared cPanel hosting advertising “unlimited” resources will underperform regardless of what you do on the WordPress side.
What I’ve Stopped Wasting Time On
A few things that show up in every “WordPress performance” post but have delivered diminishing or zero returns in my actual work:
- GZIP vs Brotli obsession. Both work. Cloudflare handles it. Stop worrying.
- Database table optimization (OPTIMIZE TABLE). Unless you have a site with millions of rows of fragmented data, the gains are negligible on modern MySQL/MariaDB. It causes table locks. Not worth the risk for a typical site.
- Minifying HTML. The savings are measured in bytes. I’ve never seen this move a metric.
- Chasing a perfect PageSpeed score. A 95 vs 100 in PageSpeed Insights does not meaningfully impact real-user experience. Core Web Vitals passing in the field data is what matters for SEO; the lab score is a diagnostic tool, not a goal.
- DNS prefetch for everything. Prefetching a hundred origins adds overhead. Be selective — prefetch the two or three domains your critical resources actually come from.
Putting It Together
Do the basics in order — hosting, PHP version, object cache, page cache, images, plugin audit — and you’ll have covered the vast majority of the gains. Everything after that is incremental.
Sites accumulate plugins, content, and debt. Performance degrades by default. The sites that stay fast are the ones where someone opens Query Monitor regularly and treats speed as ongoing maintenance, not a one-time project.