Troubleshooting

Why Is Your WordPress Site Slow? A Diagnostic Order, Not a Checklist

WordPress performance advice is usually handed over as a flat, alphabetized checklist. This walks through the same fixes in the order they actually pay off, starting with the thing most sites get wrong first: images.

Why Is Your WordPress Site Slow? A Diagnostic Order, Not a Checklist

Open a slow WordPress page in a waterfall tool and the answer is usually sitting in the first few rows, before you’ve even scrolled down to the CSS. Say a JPEG straight out of a phone camera, five or six megabytes, downloaded in full even though the theme displays it at 700 pixels wide. That single request can outweigh every plugin, every font file, and every script on the page combined.

Most answers to “why is my WordPress site slow” treat every possible cause as equally likely: minify this, defer that, switch hosts, disable half your plugins, install a caching plugin, then another one on top of it. None of that is wrong exactly, but working through it alphabetically instead of in order of actual impact means you can burn a weekend on changes that shave off, say, 50 milliseconds while the thing actually costing three full seconds sits untouched.

The order below reflects roughly how much weight each layer tends to carry on a typical WordPress site, from a personal blog to a small WooCommerce store (not a universal ranking, but a shape that holds often enough to be worth following before reaching for a 30-item checklist): images first, then hosting and caching, then plugin and query behavior, then render-blocking front-end assets, then database housekeeping.

Ranked chart of five WordPress performance layers by typical share of the win: image weight first, then hosting and page cache, an expensive plugin query, render-blocking assets, and database bloat last

Images carry the most weight, so they go first

Independent page-weight research, including the HTTP Archive’s ongoing state-of-the-web reports, has shown images as the largest single category of bytes on a typical web page for years running. WordPress sites are no exception, and often worse than average, because the CMS makes it trivially easy to drag a full-resolution photo straight from a phone into a post with no resizing step in between.

The most common cause is an oversized original that was never actually resized down. WordPress does have a safety net here: since version 5.3, any upload wider or taller than the big_image_size_threshold filter’s default of 2560 pixels gets a scaled-down “-scaled” version created automatically, and that scaled file is what’s used as the displayed “full” size, while the untouched original stays in the uploads folder for reference. It’s a reasonable default, but it only fires on upload: images added before WordPress 5.3, images uploaded through an importer that bypasses the media handler, or sites where the filter has been disabled all skip it. Our guide to how WordPress image sizes actually work walks through that mechanism and the registered-size system underneath it in more detail than fits here.

Format choice compounds the problem. A photograph saved as PNG rather than JPEG or a modern format can run several times larger for no visible quality gain, because PNG’s lossless compression is built for flat-color graphics and screenshots, not gradients and photographic detail. The reverse mistake happens too: logos and icons saved as JPEG pick up compression artifacts they don’t need. We cover which format actually fits which kind of content, including where WebP and AVIF are worth the tradeoff, in our breakdown of WebP, AVIF, JPEG, and PNG.

Then there’s responsive coverage. WordPress has generated a srcset attribute automatically since version 4.4, via wp_calculate_image_srcset(), so a phone on a narrow viewport can download a smaller file than a widescreen desktop sees. That only works, though, if the intermediate sizes it needs actually exist: a theme that strips down registered image sizes, or markup that outputs a raw <img> tag instead of going through wp_get_attachment_image(), quietly loses that coverage, and every visitor ends up downloading the desktop-size file regardless of device. Our piece on retina images and srcset covers how to check whether a theme is actually serving the right size.

Finally, a media library that’s never been cleaned up compounds all of the above: years of duplicate uploads, abandoned drafts’ images, and unused intermediate sizes sitting on disk. That mostly affects backup size and how long it takes to find anything in the library rather than front-end load time directly, but a library nobody has ever pruned is usually also where the oversized, wrong-format originals from the last few paragraphs have been quietly accumulating. Our guide to cleaning up the WordPress media library covers how to find and safely remove what’s actually unused.

If you’re looking at a media library with hundreds of oversized originals and don’t want to open each one individually in an editor, a batch image processor (like WunderPaint’s Image Processor, which handles resizing, format conversion, and quality presets across many files at once) can work through that kind of backlog considerably faster than re-uploading everything by hand.

Hosting and caching: page cache versus object cache

Once image weight is under control, hosting is usually the next biggest lever, particularly on shared hosting with no page cache in front of it. On shared hosting, your PHP processes, database connections, and CPU time are split across however many other tenants are on the same server; a traffic spike on someone else’s site can slow yours down even though nothing about your own configuration changed.

A page cache sidesteps most of that by storing the fully rendered HTML of a page and serving that stored copy directly, without WordPress, PHP, or the database being involved in the request at all. This is the single biggest caching win available, and it mainly benefits anonymous, logged-out visitors. A shopping cart, an account page, or anything else that has to be different per visitor generally can’t be served from a static page cache without extra logic layered on top.

An object cache is a different layer entirely: it caches the results of expensive function calls and database queries (a navigation menu, a set of options, a taxonomy lookup) so they don’t have to be recomputed on every request. WordPress ships with a basic object cache built in, but by default it’s non-persistent, meaning it resets at the end of every single page load and only helps avoid duplicate queries within that one request. A persistent object cache, backed by Redis or Memcached, keeps those cached values around between requests, which is where the real benefit shows up, especially for logged-in traffic that a page cache can’t touch. Budget shared hosting frequently doesn’t offer either of these as an option, which is a large part of why moving to a host that supports proper caching tends to outperform installing yet another caching plugin on infrastructure that can’t back it up.

Matrix showing which visitors each WordPress cache helps: page cache covers logged-out visitors only, a persistent object cache covers everyone, and browser cache covers returning visitors

Plugins aren’t inherently the problem: unoptimized queries are

A site running 40 well-behaved plugins can easily outperform a site running five badly written ones. Plugin count by itself is a weak predictor of slowness; what actually matters is whether any given plugin runs an expensive, uncached database query on every single page load, regardless of whether that page needs it. A contact form plugin that checks a remote API on every page render, or a “related posts” widget that runs an unindexed query across the entire posts table, does more damage than dozens of plugins that just sit quietly enqueuing a small stylesheet.

The fastest way to find the actual culprit is Query Monitor, a free plugin that adds a panel to the admin toolbar showing total query count and time per page load, which plugin or theme file each query came from, hooks fired, and any PHP warnings or notices along the way. Installing it, loading a slow page, and sorting queries by execution time usually points straight at the offending plugin within a minute or two. If you don’t want a plugin sitting on a production site even temporarily, most managed hosts and control panels expose a slow-query log at the server level that records any query over a configurable threshold, which works just as well for tracking down the source without adding overhead of its own.

Render-blocking CSS, JS, and fonts

Further down the list, but still worth doing, is cleaning up render-blocking assets. A browser has to download and parse a page’s CSS before it can paint anything, and a <script> tag without an async or defer attribute pauses HTML parsing entirely at the point it’s encountered. On a typical WordPress page, each active plugin and the theme itself may enqueue its own stylesheet and script file, and a page with a couple dozen separate CSS and JS requests queues some of them up rather than fetching everything in parallel.

Web fonts add a similar cost when loaded synchronously: a blocking @import or a render-blocking <link> for a font file delays the point at which any text becomes visible, sometimes producing a flash of invisible text while the browser waits for the font to arrive. Setting font-display: swap in the font’s CSS lets the browser show a fallback font immediately and swap in the web font once it loads, rather than hiding the text until then.

All of this is real and worth fixing, but the ceiling on how much it can save is lower than the layers above it. Combining and deferring a dozen small CSS and JS files typically recovers tens to a couple hundred milliseconds. That’s a meaningful improvement on a site that’s already lean everywhere else. It’s close to irrelevant on a site still serving multi-megabyte images to every visitor.

The render blocking part of the order is the one you can inspect without any tooling at all. View source on the page, copy it, and paste it below.

The audit counts the stylesheets and scripts in the head that hold up the first paint, names the plugins behind them from their handles, groups the third party domains and tells you which ones have a preconnect. Nothing is loaded: it reads the markup as text, which is also why it can say something useful about a page that is currently too slow to profile.

Render blocking and weight audit

Paste the source of a page and see what holds up the first paint, what the page weighs, and which other companies it asks for help. The source is read into an inert document, so nothing at all is fetched: no stylesheet, no script, no image, no font. Nothing leaves this browser tab.

Or drop the saved page
Drop an .html file here
or press Enter to pick one. The file is read in this tab and never uploaded.
Nothing to analyse yet

The findings keep the order of the diagnosis, not the order of severity: server answer, then what blocks the paint, then images, then other companies, then the rest. Work down the list, not around it.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

Database bloat: revisions and transients nobody cleaned up

The last layer is the database itself, specifically two things that accumulate silently over years rather than appearing all at once: post revisions and expired transients. By default, WordPress keeps an unlimited number of revisions for every post and page: every autosave and every manual save creates a new row in wp_posts, and on a site that’s been publishing and editing for five or six years, that table can end up carrying far more revision rows than actual published content.

The scale is easy to underestimate, so here is a real count rather than an estimate. This site has 44 published posts and pages and has been running for about ten weeks. Its wp_posts table holds 2,009 rows, and 1,168 of them (58 per cent) are revisions. Nobody did anything wrong to produce that; it is simply what the default setting does while you write.

Chart of a real WordPress database where wp_posts holds 2,009 rows, of which 1,168 are post revisions and only 44 are published posts and pages

You can cap that going forward with a single constant in wp-config.php, placed above the line that says “That’s all, stop editing”:

// Keep the 5 most recent revisions per post instead of unlimited
define( 'WP_POST_REVISIONS', 5 );

That limits how many new revisions accumulate from this point forward; it won’t retroactively trim ones already stored, which is a separate cleanup task best done with a plugin or a direct database export as a precaution first.

Transients are the other slow accumulator. WordPress uses them as a temporary cache: a value stored with an expiration, meant to be recalculated once it goes stale. In practice, expired transients aren’t deleted proactively; a transient only gets cleaned up when something actually tries to read it again and finds it’s expired. A transient nothing ever reads again just sits in wp_options indefinitely. If you have WP-CLI access, clearing those out is a single command:

wp transient delete --expired

A quick note on where each of these belongs, since it’s a common mix-up: the revisions constant is a wp-config.php setting, not something you add to your theme’s functions.php. If you’re new to editing either file, our explanation of what functions.php actually does covers where different kinds of code snippets belong and how to avoid a stray character taking the whole site down.

When something looks wrong

The homepage is fine, but one specific blog post crawls. This is almost always local to that post rather than a site-wide issue: an embedded gallery, an oversized hero image, or an old embed loading a third-party script. Check that post’s own media before touching any global setting.

Time to first byte is slow even on a page that should be cached. A page cache only speeds up the response once it’s been generated once; if the very first byte is slow, the delay is happening at the server or hosting layer before caching ever gets a chance to help, which points at hosting resources rather than a WordPress configuration issue.

The site feels fast logged out and noticeably slower logged in as an admin. Most page caches only serve logged-out visitors by design, since a logged-in session and its admin bar are unique per user and can’t safely be cached as static HTML. Slowness that only shows up while logged in is a real signal about plugin or query performance, not a caching gap.

The wp_options table has grown to an unusual size. This is frequently caused by one or two options set to autoload, meaning WordPress loads them into memory on every single request whether that particular page needs them or not. A plugin storing a large serialized array with autoloading turned on can quietly cost more than a dozen ordinary plugins combined.

Where this leaves you

None of these five layers is optional forever: a genuinely fast site eventually needs attention paid to all of them. But the order matters more than the completeness of the list. Minifying and combining a handful of CSS and JS files on a site whose real problem is a media library full of six-megabyte camera originals barely moves the needle, because the bottleneck was never the number of requests in the first place.

Start where the bytes actually are. For most unoptimized WordPress sites that means images, then whatever the hosting and caching setup is or isn’t doing, then whichever plugin is running an unnecessary query on every load, and only after that the smaller, still-worthwhile cleanup of render-blocking assets and years of accumulated database cruft.

Work through it in that order and each fix tends to be visible immediately, because you’re spending the effort where the weight actually is rather than where it’s easiest to find a plugin that promises to help.

Why Is Your WordPress Site Slow? A Diagnostic Order, Not a Checklist

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

Developer Tools

Hashes and Encoding: Getting Back Into Your Own WordPress

The reset mail never arrives and you are locked out of a site you own. The way back in runs through one column: user_pass in wp_users. Here is what is actually stored there, checked line by line against wp-includes/pluggable.php, plus a browser-only tool that builds both the bare MD5 and the modern $wp bcrypt value.

Troubleshooting

Allowed Memory Size Exhausted: Where the WordPress Memory Limit Lives

The "Allowed memory size exhausted" fatal error involves three separate ceilings, and the one most people raise is not the one that stopped the request. How PHP's memory_limit, WP_MEMORY_LIMIT and WP_MAX_MEMORY_LIMIT interact, and why images trigger the error more often than anything else.

Developer Tools

Favicon Generator: The Icon Files a Site Actually Needs

The favicon is the one image on your site nobody checks at the size everybody sees. Here is the short list of files a site still needs, the two measurements that decide whether your mark survives a browser tab and an Android mask, and exactly where the WordPress site icon stops.

Troubleshooting

WordPress Maximum Upload Size: Where the Limit Really Lives

The media library says 8 MB, the file is 12 MB, and nothing you edit in WordPress moves the number. It is not a WordPress setting at all: core reads two PHP directives and prints the smaller one. Which limit is really stopping the upload, why post_max_size is usually the culprit, how to raise the limits in the order that works, and why a photograph almost never needs a bigger ceiling.

Speed & Performance

A Web Performance Budget for Your WordPress Site

Pick a connection profile and a target load time, and arithmetic hands you a byte budget per resource class. A web performance budget is a decision tool: it tells you which image, which font and which plugin gets told no, before the page ever gets slow.

WordPress Images

WordPress Image Sizes Explained: Every File Created From a Single Upload

One upload becomes eight or more files. Here is what WordPress actually creates, why some sizes are missing, and what to do about it, with numbers measured on a real site.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

Pro

3D Earth Studio

A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

Free

Mystic Studio

Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

Free

Marble Bath

Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

Free

Day Ring

Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.