Performance Optimization 21 min read

How to Fix a Slow WordPress Site (Real 2026 Case Study + 6-Step Fix Sequence)

How to Fix a Slow WordPress Site (Real 2026 Case Study + 6-Step Fix Sequence)

WordPress site loading slowly? This 2026 guide walks through the exact six-step fix I use on real client sites — server, plugins, images, caching, JavaScript, database — with measurable before-and-after numbers from a real audit (Lighthouse 38 baseline, LCP 3.5-5.5s), not generic advice.

Why WordPress sites get slow

After 13 years and more than 100 WordPress builds, the slow-site pattern is almost never WordPress itself. It is what sits on top of WordPress: a heavy theme, unoptimised images, a long plugin list, and cheap shared hosting. Together those four layers turn a 500ms server into a 4-second page.

The four specific culprits I see on every problem site:

  • Shared hosting with no PHP opcode caching. On a shared server, each PHP request can take 300ms to 800ms just for opcode compilation. That is before a single database query runs. A site on shared hosting with 20 plugins easily hits 3 to 5 seconds uncached.
  • Uncompressed images at full resolution. A homepage with six to eight JPEG hero images often ships 3MB to 6MB of image data. On a median mobile connection that is 4 to 8 seconds of transfer alone.
  • Plugins loading assets everywhere. A social-share plugin enqueuing 200KB of JavaScript on a blog post where it is not even visible is a real pattern I find weekly. Multiply by 5 to 10 poorly built plugins and you are adding 1MB to 2MB of render-blocking assets on every page.
  • No object cache, so WordPress hammers MySQL on every request. WordPress core generates 30 to 80 database queries per page. Without Redis or Memcached, every one of those hits MySQL. On a busy site that is thousands of queries per minute against a single-threaded query cache.

Google considers a 2.5-second Largest Contentful Paint the cut-off between good and poor user experience. Above that, rankings drop and conversion rates fall measurably. Every layer has a known, repeatable fix.

WordPress Performance Stack Request path from visitor to database — fastest layers at top Browser Cache Static assets served locally — CSS, JS, images never hit the network on repeat visits 0ms repeat visits CDN Edge (Cloudflare / BunnyCDN) Cached pages and media served from 200+ edge nodes — visitor never reaches origin 20–50ms TTFB from edge Nginx + FastCGI Cache Full HTML pages cached on disk — PHP never runs for cached requests <50ms cached HTML PHP-FPM + WordPress Uncached PHP execution — plugins, hooks, and theme run on every cache miss 200–800ms uncached PHP MySQL + Redis Object Cache Query layer — Redis serves cached results in RAM; MySQL handles misses and writes 5–50ms query layer
The WordPress performance stack from visitor to database. Each layer above catches requests before they reach the layer below. A well-configured stack keeps most traffic at the CDN or FastCGI cache level, well above the expensive PHP-MySQL layer.

Measure before you touch anything

Before touching a single config file, get baseline numbers. Without measurements the work is guesswork and the gains are invisible.

The four tools that cover every angle:

  • Google PageSpeed Insights — reports the Core Web Vitals that Google actually ranks on. Numbers to watch: LCP above 2.5s is poor; CLS above 0.1 is poor; INP above 200ms is poor. Run it on mobile, not just desktop.
  • GTmetrix — shows a waterfall of every asset with timings. TTFB above 600ms means the problem is server-side, not frontend. TTFB above 200ms but below 600ms means caching or database. TTFB under 200ms means focus on assets.
  • Query Monitor (free WordPress plugin) — lists slow database queries, duplicate queries, and plugin-induced overhead inside the admin. Sort by query count to find offenders immediately.
  • Chrome DevTools Network panel — reveals render-blocking resources, resource priorities, and unused chunks that third-party tools miss.

Document starting numbers for every URL type that matters: homepage, archive, product page, cart. Re-measure after each change. If a change does not move the numbers, it did not work. This sounds obvious but most clients I take on have never measured systematically and are surprised when the actual bottleneck is not what they assumed.

Case study: what a truly slow WordPress site looks like

Numbers help. The WaterEgo kayaking community site — an Irish outdoor-activity portal I audited in 2026 — is a textbook example of every layer stacking against performance at once. The measurements below are the actual baseline, not projections.

  • Lighthouse mobile score: 38 / 100. Deep in the red band.
  • Largest Contentful Paint: 3.5 – 5.5 seconds. Google's cut-off is 2.5 seconds.
  • 408 KB of unminified CSS render-blocking on every page.
  • 192 KB main.js — 4,388 lines — parsed on every page whether the page used the code or not.
  • GSAP and Leaflet loaded on every route, including routes with no map and no animation.
  • Hero rendered as a CSS background-image, which is invisible to the preload scanner. That single decision cost 400–800 ms of LCP.
  • A requestAnimationFrame cursor loop running continuously on mobile, burning 5–10 ms per frame on devices that had no cursor to begin with.

None of that is unusual. It is the accumulated cost of premium themes, portfolio-inherited habits, and demos that were never meant to ship together. The six-step fix sequence below — server, plugins, images, caching, JavaScript, database — attacks each layer in turn, and each step moves a measurable number.

Step 1 — Fix the server first (TTFB under 200ms)

If Time to First Byte sits above 600ms, no amount of frontend tuning will rescue the page. Server is layer one, and it is the layer most people skip because it feels harder than installing a plugin.

The stack I deploy on every serious site is the same pattern running on the Fine Luxury Property platform: a managed VPS (DigitalOcean, Hetzner, or similar), Nginx in front of PHP 8.2 FPM, Redis as the WordPress object cache, and a tuned MySQL. That combination reliably drops TTFB into the 80 to 200ms range. Shared hosting will never hit those numbers because the underlying machine is sharing CPU and I/O with hundreds of other tenants.

PHP-FPM pool configuration

The default PHP-FPM pool settings are conservative and throttle throughput. On a VPS with 4GB of RAM, these settings handle traffic spikes without hitting process limits:

[wordpress]
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.max_requests = 500
pm.process_idle_timeout = 10s
request_terminate_timeout = 60s

The pm.max_requests = 500 setting recycles workers after 500 requests, which prevents PHP memory leaks from accumulating. On a shared server you do not get to set this. On your own VPS it takes five minutes and immediately improves stability under load.

PHP OPcache settings

OPcache compiles PHP once and stores bytecode in shared memory. Without it, PHP recompiles every file on every request. These settings belong in your php.ini:

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
opcache.save_comments=1
opcache.fast_shutdown=1

Set opcache.memory_consumption to at least 128MB on any real WordPress install. A large site with many plugins can need 256MB. After setting these, run php -i | grep opcache to confirm they loaded correctly.

If a VPS is not in scope right now, the next best step is a managed WordPress host: Kinsta, WP Engine, or Cloudways. They run this same kind of stack and handle the operations. What you cannot do is stay on shared hosting and expect professional-grade performance.

Step 2 — Audit and prune plugins

A typical WordPress site I am called in to fix runs 25 to 35 plugins. Most professional sites ship cleanly with 8 to 12. The extra plugins are not neutral — each one adds hooks, database queries, and often assets on every page regardless of whether that page needs them.

Open Query Monitor, sort by query count descending, and identify the worst offenders. The usual suspects:

  • Social-share plugins enqueuing scripts on every page
  • Slider plugins shipping multiple megabytes of JavaScript and CSS
  • Page builders loading their full asset bundle site-wide, even on pages that do not use them
  • Analytics plugins firing trackers from PHP instead of using Google Tag Manager
  • Form plugins loading assets on every page when the form lives on one page only
  • SEO plugins configured to run full analysis on every page load

Heavy plugins vs lightweight alternatives

Heavy pluginTypical weightLightweight alternative
Jetpack (full suite)500KB+ JS/CSS per pageIndividual modules or standalone plugins per feature
Contact Form 7 + addons80–200KB unconditionedWPForms Lite, or WP Simple Contact Form (load on form page only)
Slider Revolution400KB–1MB JSCSS-only hero with a single transition
WooCommerce Social Login200KB+Native OAuth via theme or headless approach
All-in-One SEORuns 15–20 queries/pageYoast SEO or Rank Math on slim configuration
Smush (with bulk processing)30–50 DB queries per media uploadCLI WebP conversion via ImageMagick (one-time, no overhead)

Replace what you can, and conditionally load what remains. In WordPress you can wrap wp_enqueue_scripts calls in a conditional check so the plugin's CSS and JS only load on the pages where they are needed. That alone often cuts 200KB to 500KB from pages that previously loaded every plugin's assets blindly.

Step 3 — Compress and serve modern image formats

On a typical WordPress homepage, images make up 60 to 80 percent of the total transferred bytes. Image optimisation is the single biggest quick win on most sites, and it requires zero server configuration to start.

The image checklist I apply to every site:

  • Serve WebP or AVIF instead of JPEG and PNG. On this portfolio site, converting six project PNGs to WebP cut their combined weight by roughly 92 percent.
  • Generate responsive image sets so phones do not download desktop-resolution files. Use WordPress's built-in srcset and register appropriate image sizes for your theme.
  • Lazy-load everything below the fold with loading="lazy". WordPress adds this automatically since version 5.5, but check that your theme does not override it.
  • Declare explicit width and height on every <img> element. Without these attributes the page reflows as images load, destroying Cumulative Layout Shift scores. This is one of the most common CLS culprits I find on audits.
  • Preload the LCP image. Add a <link rel="preload"> for the above-the-fold hero image so the browser prioritises it over other assets.

Bulk WebP conversion with ImageMagick

For existing sites with hundreds of uploaded images, I run a one-time ImageMagick batch conversion rather than relying on a plugin doing it on the fly. This approach has zero runtime overhead once complete:

# Convert all JPEGs in uploads to WebP (80% quality, good for photos)
find /var/www/html/wp-content/uploads -name "*.jpg" -exec   convert {} -quality 80 {}.webp \;

# Convert all PNGs (lossless mode for logos and screenshots)
find /var/www/html/wp-content/uploads -name "*.png" -exec   convert {} -define webp:lossless=true {}.webp \;

# Verify average size reduction
du -sh /var/www/html/wp-content/uploads/*.jpg | tail -5
du -sh /var/www/html/wp-content/uploads/*.webp | tail -5

After conversion, configure Nginx to serve the WebP version when the browser supports it via the Accept: image/webp header check. No PHP involved, no plugin overhead, and browsers that do not support WebP automatically get the original.

Step 4 — Three-layer caching

Caching means serving prebuilt pages instead of executing PHP and MySQL queries on every visit. A page that takes 800ms to generate uncached will serve in under 50ms from a warm cache. The math is decisive and the configuration is not complicated once you understand the three distinct layers.

Layer 1: Page cache (the biggest gain)

Full-page caching stores complete HTML responses on disk or in Nginx's FastCGI cache. Subsequent requests for the same URL get the stored HTML without PHP running at all. On a VPS with Nginx, I use FastCGI cache directly in the Nginx config. For shared or managed hosting, WP Rocket's page cache is the most reliable plugin implementation I have tested, followed by LiteSpeed Cache on LiteSpeed servers.

One cache rule matters above all: never cache pages for logged-in users or pages with query strings like cart and checkout. Those must bypass the cache entirely or visitors will see each other's data.

Layer 2: Object cache with Redis

WordPress stores the results of database queries in an object cache. By default that cache lives in PHP memory and is discarded at the end of every request. That means the same query runs again on the next request. Redis keeps the object cache in persistent RAM so it survives across requests.

Setting up Redis as WordPress's object cache requires three steps. First, install the Redis server on your VPS:

sudo apt install redis-server -y
sudo systemctl enable redis-server
sudo systemctl start redis-server
redis-cli ping  # should return PONG

Second, install the Predis or phpredis PHP extension, then install the Redis Object Cache plugin (Tillman Recorder's plugin from wordpress.org). Third, add the connection constants to wp-config.php before the /* That's all */ line:

define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_TIMEOUT', 1 );
define( 'WP_REDIS_READ_TIMEOUT', 1 );
define( 'WP_REDIS_DATABASE', 0 );
define( 'WP_CACHE', true );

After enabling the drop-in from the plugin settings page, the object cache is persistent. On a WooCommerce store that was running 120 database queries per page, Redis dropped that to 18 per page on repeat visits. The transient lookups, term cache, and user meta queries all resolved from RAM instead of MySQL.

Redis also eliminates a specific WordPress performance drain: expired transients stored in wp_options. Without Redis, WordPress stores and queries transients in the database. With Redis, transients live in memory and expire automatically. The wp_options table stops growing indefinitely.

Layer 3: Browser cache

Proper Cache-Control headers prevent returning visitors from re-downloading static assets. In Nginx add these rules for the relevant file types:

location ~* \.(css|js|woff2|woff|ttf)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}
location ~* \.(jpg|jpeg|png|webp|gif|svg|ico)$ {
    expires 6M;
    add_header Cache-Control "public";
}

With a CDN in front (covered in the next step), the CDN respects these headers and caches assets at the edge, further reducing origin requests.

Step 5 — CDN setup

A Content Delivery Network serves your assets from servers geographically close to the visitor. Without a CDN, every request for an image or a CSS file travels to your origin server wherever it physically sits. With a CDN that same asset loads from an edge node 20 to 50ms away from the visitor regardless of where your server is.

TTFB by Hosting Type Time to First Byte — lower is better. Target: under 200ms. Shared hosting Managed WP Optimised VPS VPS + Redis + FastCGI cache 0 500ms 1000ms 1500ms 2000ms <200ms = Good 600–2000ms 150–400ms 80–200ms 30–100ms Shared Managed WP Opt. VPS VPS + Redis + Cache
TTFB ranges by hosting configuration. Shared hosting rarely achieves sub-600ms TTFB regardless of caching plugins. A properly configured VPS with Redis object cache and FastCGI page cache reaches 30 to 100ms consistently.

Cloudflare free tier setup

Cloudflare's free plan is the right starting point for most WordPress sites. It handles DNS, DDoS mitigation, basic WAF rules, and caches static assets at the edge globally. Setup takes about 20 minutes:

  1. Create a Cloudflare account and add your domain. Cloudflare scans existing DNS records automatically.
  2. Change your domain's nameservers to the two Cloudflare assigns. Allow up to 24 hours for propagation.
  3. In SSL/TLS settings, set the encryption mode to Full (strict). This requires a valid SSL certificate on your origin server. Never use Flexible — it sends plaintext from Cloudflare to your server.
  4. Under Caching > Configuration, set the Browser Cache TTL to 1 year for static assets.
  5. Create a Page Rule for yourdomain.com/wp-admin/* with Cache Level set to Bypass. Admin pages must never be cached.
  6. Enable Argo Smart Routing if budget allows — it routes requests over Cloudflare's private backbone and typically cuts TTFB by a further 20 to 30 percent.

One important Cloudflare gotcha: by default Cloudflare does not cache HTML. To cache full pages at the edge you need to add a Cache Everything page rule for your public-facing URLs, combined with a bypass rule for logged-in users (check for the wordpress_logged_in_ cookie). Without that rule, only static assets are served from the edge.

BunnyCDN for media offloading

For media-heavy sites, BunnyCDN is an excellent complement to Cloudflare. BunnyCDN is a pure CDN (no DNS proxy, no WAF) with pricing around 0.01 USD per GB for European and North American traffic. It excels at serving images, video, and large file downloads from its global pull zones.

The workflow: configure BunnyCDN as a pull zone pointing at your origin's uploads directory. Then configure WordPress to rewrite media URLs from /wp-content/uploads/ to your BunnyCDN pull zone URL. The CDN Manager plugin handles this automatically. Images load from BunnyCDN's edge; your origin only serves the first uncached request per region.

Step 6 — Defer render-blocking resources

JavaScript that loads in the <head> blocks the HTML parser. The browser stops building the DOM until the script downloads and executes. Even a 50KB script from a legitimate source can cost 300ms to 500ms on a mobile connection if it is parser-blocking.

The fixes in order of impact:

  • Move scripts to the footer wherever possible. In WordPress, enqueue scripts with in_footer: true in wp_enqueue_scripts.
  • Add defer to scripts that do not depend on DOMContentLoaded firing first.
  • Add async to completely independent third-party scripts like analytics and chat widgets.
  • Inline critical CSS for above-the-fold content. Defer the full stylesheet load.
  • Remove unused CSS. Most premium themes ship 70 to 80 percent of their stylesheet untouched on any given page.

Adding defer to WordPress-enqueued scripts

WordPress does not support adding attributes like defer to enqueued scripts natively before WordPress 6.3. For earlier versions, filter the script tag after output:

add_filter( 'script_loader_tag', function( $tag, $handle, $src ) {
    // List of script handles to defer
    $defer_handles = array(
        'my-plugin-script',
        'woocommerce',
        'jquery-migrate',
    );
    if ( in_array( $handle, $defer_handles ) ) {
        return str_replace( ' src', ' defer="defer" src', $tag );
    }
    return $tag;
}, 10, 3 );

Be careful with jQuery. Deferring jQuery while other scripts depend on it executing synchronously will break those scripts. Always test after applying defer to any script that other scripts depend on. On WordPress 6.3 and later, use the wp_script_add_data function with the strategy key set to defer or async directly — no filter hack required.

Step 7 — Database cleanup

A WordPress database that has been running for years without maintenance accumulates significant junk. Post revisions, expired transients, orphaned post meta, spam comments, abandoned auto-drafts. Each row adds to the size of wp_posts and wp_postmeta, slowing every query that scans them.

Before running any cleanup queries, take a database backup. These DELETE statements are irreversible.

Limit post revisions going forward

Add this to wp-config.php before doing anything else. Without it, revisions will accumulate again after cleanup:

define( 'WP_POST_REVISIONS', 5 );

Delete post revisions beyond the limit

-- Delete all post revisions
DELETE FROM wp_posts WHERE post_type = 'revision';

-- Clean up orphaned postmeta from deleted revisions
DELETE FROM wp_postmeta
WHERE post_id NOT IN (SELECT ID FROM wp_posts);

Delete expired transients

Expired transients in wp_options are a major source of table bloat on sites that have run plugins using transients for caching (which is most sites). This deletes both the transient and its expiry record:

DELETE FROM wp_options
WHERE option_name LIKE '_transient_%'
  AND option_name NOT LIKE '_transient_timeout_%'
  AND option_value IS NOT NULL
  AND (
    SELECT option_value
    FROM wp_options AS t
    WHERE t.option_name = CONCAT('_transient_timeout_', SUBSTRING(option_name, 12))
  ) < UNIX_TIMESTAMP();

-- Also delete the corresponding timeout records
DELETE FROM wp_options WHERE option_name LIKE '_transient_timeout_%'
  AND option_value < UNIX_TIMESTAMP();

Optimise tables after cleanup

OPTIMIZE TABLE wp_posts;
OPTIMIZE TABLE wp_postmeta;
OPTIMIZE TABLE wp_options;
OPTIMIZE TABLE wp_comments;

Add missing database indexes

On large WooCommerce stores, two missing indexes alone account for multi-second query times. These add the indexes if they do not exist:

-- Index on wp_postmeta(meta_key) — speeds up all meta_key lookups
ALTER TABLE wp_postmeta ADD INDEX meta_key_idx (meta_key(191));

-- Index on wp_options(autoload) — speeds up option loading on every page
ALTER TABLE wp_options ADD INDEX autoload_idx (autoload);

On a WooCommerce store with 50,000 orders, adding the wp_postmeta(meta_key) index turned a 2.4-second order query into a 22ms query. That single change dropped the page's total database time from 3.8 seconds to 0.4 seconds. The same fix applies to sites with large post archives, member directories, or event listings.

Step 8 — WordPress Multisite speed

If you are running WordPress Multisite and wondering why it is slower than a single-site install, the answer is architectural. Multisite was designed for networks, not performance, and it introduces three bottlenecks that a single-site install does not have.

Per-site table bloat

Every site in a Multisite network gets its own set of tables: wp_2_posts, wp_2_postmeta, wp_2_options, and so on. A network of 50 sites has 50 separate wp_options tables, each potentially containing thousands of autoloaded rows. The database cleanup queries above must be run for each site's table set, not just the main tables.

The practical solution is to script the cleanup across all sites using WP-CLI's --url flag to target each site in the network:

# List all sites in the network
wp site list --fields=blog_id,url

# Run cleanup on each site by blog_id
wp --url=subsite.example.com transient delete --expired
wp --url=subsite.example.com cache flush

sunrise.php hook overhead

Multisite domain mapping loads a sunrise.php drop-in early in the WordPress load cycle. If this file contains database queries to resolve domain mappings — which many domain mapping plugins do — it adds a database round-trip before WordPress has even loaded. On a network with mapped domains on every site, this can add 50ms to 150ms to every request.

The fix is to cache domain mappings in Redis or a static PHP array. The MU Domain Mapping plugin (or the more recent Mercator library) supports Redis-backed domain resolution. Configure it and the overhead drops to a single Redis lookup at approximately 1ms rather than a MySQL query at 20ms to 80ms.

Network-wide object cache configuration

On Multisite, the Redis object cache must be configured to use separate key namespaces per site. Without namespacing, sites can read each other's cached objects, which causes subtle display bugs and incorrect data. Add the blog ID prefix to your wp-config.php Redis configuration:

define( 'WP_REDIS_PREFIX', 'site_' . get_current_blog_id() . '_' );
define( 'WP_REDIS_SELECTIVE_FLUSH', true );

The WP_REDIS_SELECTIVE_FLUSH constant ensures that flushing the cache on one site in the network does not flush the caches of all other sites — a critical setting on production networks where cache warming takes minutes.

Shared plugin and theme loading

Network-activated plugins load on every site in the network. If a plugin is only needed on three of 50 sites, network-activating it adds overhead to the other 47. Audit network-activated plugins regularly and activate per-site those that are not universally needed. Each plugin removed from network activation saves its hook registration time multiplied by every page load on every site.

What results does this process actually produce?

In 13 years across 100+ builds, the pattern is consistent. Here are the headline numbers from real sites:

  • RealHomes Modern, the bestselling ThemeForest real estate theme I helped build at Inspiry Themes, was architected on this performance stack from launch. It consistently holds above-90 PageSpeed scores while serving thousands of agencies globally, many running property databases of 10,000 to 50,000 listings.
  • Fine Luxury Property, the platform I currently maintain, saw Core Web Vitals improve by 50 percent or more with zero design changes after applying the server, image, and caching work described above. The VPS stack replaced shared hosting and the TTFB dropped from 1.2 seconds to under 150ms on every page.
  • AzanGuru, a learning platform I built that now serves 100K+ Android installs, runs the WordPress plus Redis stack for its backend API. Database query time per API call dropped from 180ms average to 22ms after adding the Redis object cache and the postmeta index described in Step 7.
  • This portfolio — the WebP conversion alone dropped the homepage payload by more than 300KB, and the explicit width/height sweep on every project card image eliminated CLS from 0.14 to 0.01 across the entire site.

The pattern holds on every engagement: fix the server, clean up plugins, optimise images, add caching at all three layers, set up a CDN, defer JavaScript, and clean the database. Skip any layer and the gains stall. Apply all layers and a 4-second site becomes a sub-second site.

Common mistakes to avoid

After reviewing dozens of failed WordPress performance projects, the same errors appear repeatedly. These mistakes waste hours of work and sometimes make performance worse.

  1. Measuring on localhost or staging. Local development environments have no network latency, warm caches, and different hardware from production. A site that scores 98 on PageSpeed on localhost will score 55 on the actual production server over mobile. Always measure on production, from a real location, using Lighthouse in throttled mode.
  2. Adding a caching plugin without enabling the object cache. A page cache alone still lets WordPress hammer MySQL for every user-specific query — WooCommerce cart, member session, search results. Without Redis or Memcached as the object cache, the page cache only solves half the problem.
  3. Pointing a CDN at uncompressed assets. A CDN serves files faster but does not compress them. If your origin is serving 2MB of uncompressed CSS and images, the CDN will serve that same 2MB faster — not smaller. Enable Brotli or gzip at the origin (and Cloudflare's compression settings) before measuring CDN impact.
  4. Running database cleanup without limiting revisions first. Deleting 50,000 revisions means nothing if the site generates 200 new ones the following week. Always set WP_POST_REVISIONS first, then clean. Otherwise the cleanup is a one-time fix on an ongoing problem.
  5. Deferring all JavaScript blindly. Deferring scripts that initialise UI components — sliders, tabs, accordions — will break those components visibly on first load because the DOM will be painted before the scripts run. Test every deferred script on real pages. Analytics and chat widgets are safe to defer. jQuery-dependent UI scripts are not unless you have confirmed their dependencies are also deferred in the correct order.

Frequently asked questions

Why is my WordPress site so slow after adding plugins?

Most WordPress plugins register hooks, run database queries, and enqueue scripts on every page load regardless of whether that page actually uses the plugin's functionality. A single poorly-built plugin can add 10 to 30 database queries and 200KB of render-blocking assets on every request. When you install 10 to 20 plugins, that overhead compounds. Install Query Monitor and look at the database query count and total query time to identify which plugins are the worst offenders. The fix is either replacing heavy plugins with lighter alternatives, conditionally loading them only on relevant pages, or removing them entirely if they are not providing value proportional to their cost.

What is a good TTFB for WordPress?

A TTFB under 200ms is the target for a well-optimised WordPress site. Google's own guidance from the Core Web Vitals documentation rates TTFB under 800ms as acceptable for SEO purposes, but in practice a TTFB above 400ms usually indicates a server or caching problem. With a properly configured VPS running Nginx, PHP-FPM, Redis, and FastCGI page cache, I consistently see TTFB of 50ms to 150ms on real production sites. On Cloudflare-fronted sites with a warm cache, edge TTFB is typically 20ms to 50ms regardless of origin server location.

Does WordPress caching really make a difference?

Yes, and the difference is not marginal — it is transformative. An uncached WordPress page running 30 to 80 database queries, executing all active plugin hooks, and rendering PHP templates typically takes 500ms to 2,000ms to generate, depending on server quality and plugin count. That same page served from a FastCGI cache typically returns in 30ms to 80ms. That is a 10x to 60x improvement in response time from a single layer of caching. Adding Redis object cache reduces the database load for uncached requests, typically cutting PHP execution time by 40 to 70 percent. The combination of page cache, object cache, and browser cache is not optional on any site that needs to perform.

How do I speed up WordPress multisite?

WordPress Multisite has three performance-specific problems: per-site table bloat in the database, domain mapping overhead in sunrise.php, and network-activated plugins running on every site regardless of need. The practical fix is to run database cleanup on each site's table set using WP-CLI's --url flag, replace any sunrise.php domain mapping that queries MySQL with a Redis-backed resolver, and audit network-activated plugins so only universally-needed plugins run at the network level. Configure the Redis object cache with per-site key prefixes using WP_REDIS_PREFIX and WP_REDIS_SELECTIVE_FLUSH to isolate site caches and prevent cross-site cache pollution. These four steps together typically reduce per-request time by 30 to 60 percent on networks with more than five sites.

Should I use Cloudflare for WordPress?

Yes, Cloudflare's free tier is the right starting point for almost every WordPress site. It provides global CDN for static assets, DDoS mitigation, a basic Web Application Firewall, and free SSL. The configuration does require attention: set SSL mode to Full (strict), bypass cache for all WordPress admin and WooCommerce cart/checkout pages, and exclude cookies like wordpress_logged_in_ from cache eligibility to prevent logged-in user pages from being cached and served to anonymous visitors. The most common Cloudflare mistake is leaving the SSL mode at Flexible, which sends traffic from Cloudflare's edge to your origin over plaintext HTTP. Use Full (strict) from day one.

How long does it take to speed up a WordPress site?

A typical WordPress performance engagement takes 2 to 5 days of focused work for a site that needs all layers addressed: server migration or configuration, plugin audit, image conversion, caching setup, CDN configuration, JS deferral, and database cleanup. Server migration is the longest step if it involves moving from shared hosting to a VPS. Image conversion and database cleanup can be completed in a few hours on most sites. The caching setup — page cache, Redis object cache, and CDN — takes about half a day to configure correctly and test. The common exception is WooCommerce sites, which need additional testing to ensure caching does not interfere with cart and checkout flows; those typically take a full extra day of validation.

Also Read

Need a WordPress performance audit?

If your WordPress site is slow and you want the full stack reviewed properly, get in touch. I have shipped this exact playbook on 100+ projects, including the Fine Luxury Property platform and the AzanGuru learning platform that now serves 100K+ Android installs. I will tell you exactly which layer is the bottleneck and what it will take to fix it.

Written by Sungraiz Faryad

Full Stack Developer with 13+ years building enterprise WordPress solutions, web applications, and custom plugins. Currently available for freelance projects.

Hire Me

Need Help With This?
Let's Work Together

If you're facing challenges like those discussed in this article, I can help. 13+ years of experience, 100+ projects delivered.

Get in Touch