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.
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.
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 press Enter to pick one. The file is read in this tab and never uploaded.
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.
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.
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.