Skip to content

Blog

WordPress Isn’t Slow. YOUR WordPress Is.

Edit: I’ve added a new section for PHP memory issues and Cleared every cache? It might be their end “WordPress is slow” is an infamous statement that is uttered by clients, agencies pitching headless rebuilds, people saying that AI has made the CMS irrelevant, and developers who inherited the slowest mess of a website from…

Edit: I’ve added a new section for PHP memory issues and Cleared every cache? It might be their end

“WordPress is slow” is an infamous statement that is uttered by clients, agencies pitching headless rebuilds, people saying that AI has made the CMS irrelevant, and developers who inherited the slowest mess of a website from their predecessors.

It’s mostly false, however. As far as I can remember, WordPress development started when the CMS was a fork of b2, and all the slow websites that I have worked on had only a few common reasons for that issue, and none of them were caused by WordPress itself. Years of accumulation are what caused websites to be slow – plugins that have not been uninstalled, code that has not been revisited after being initially written, and fix upon fix.

Here is where the speed disappears and how to restore it.

Contents

First, two kinds of slow

It’s useful to differentiate two problems which are often confused.

“Slow to respond” refers to the period before the server starts responding, known as Time To First Byte (TTFB). It’s PHP, MySQL, plugins, and hosting.

“Slow to render” refers to all content afterwards – images, CSS, JavaScript and third-party scripts which need to be downloaded by your browser.
Your website may have trouble with one or the other, while being perfectly okay at the opposite thing. Most complaints about “slow WordPress” websites actually refer to one of these things, and solutions are very different.

1. The database: the problem you can’t see

The database structure is good for WordPress. Not what goes inside it.

One thing that gets stuffed inside without anyone noticing is the autoloaded options. wp_options table has an autoload option. Whatever has a value set to “yes” in it is pulled into the memory for each and every page load regardless of whether it needs to be or not. Plugins adore this option. I can easily find sites carrying several MBs worth of autoloaded information per page request, much of it related to plugins that have long been uninstalled. Uninstalling doesn’t imply cleanup, apparently.

And of course, there is wp_postmeta. This is the key-value table used for storing custom fields. It is flexible enough which is why everyone uses it and totally unsuitable for querying like a real table.
Throw in a bunch of expired transients that were not deleted, unlimited post revisions (400 edits of one page equals 400 posts), leftover meta and abandoned WooCommerce sessions and we have quite a burden for our poor database here.


A real world example: 3,000,000 rows of contact forms

The customer was using Contact Form 7 with the companion plugin Flamingo, which kept a copy of every form submission stored in the database. This is good practice if the email happens to not make it through. The idea was to clean out form submissions older than 30 days. The customer wanted to change it to 90 days, which is fair because it’s their data and 90 days seems like a reasonable request.

The site became slower. But not just a couple of milliseconds slower and only visible in a performance test. No, it slowed down significantly; pages loaded in 5 to 10 seconds and there were console errors, time outs, your the worst nightmare basically.

The way Flamingo keeps track of form submissions is as posts in the database. Every field in the forms is saved as a separate row in the post meta table. Add a high traffic contact form, some spam, and time into the mix and you get a database with more than 3,000,000 rows in wp_postmeta.

“Overhead” was the key to this issue. Even when there were deletions, it didn’t mean that the space used by MySQL would be freed. The table was still bloated and fragmented, which affected all the queries. This was not reflected anywhere in the WordPress administration panel; there was no way to know about it until a direct database request was made via phpMyAdmin or some queries to the table.

I’ve never been able to explain “overhead” without lots of erm’s and ah’s, so my best analogy at 6:24am: MySQL overhead is like a cake that’s mostly air. From the outside you’ve still got a perfectly respectable cake, but once you start taking slices, you discover that a surprising amount of what you thought was cake is actually structural overhead.

Once I got rid of all the old post_meta data and optimized the tables, the site was back to being fast and responsive without any console errors or timeouts. No changes besides that; there was no new hosting, plugin, or anything else. It was just the result of uncontrolled growth of the database. To prevent this from happening again in the future, I wrote a script which allows me to save the old submissions outside of the database and delete them periodically.

This does not mean that Flamingo is bad. This is exactly what it has been programmed to do. The point here is that any module that writes to the database on each visitor interaction will grow until it gets managed by someone, and WordPress will never warn you when it happens.


Solution: Audit auto-loading data, clean orphaned tables and options, limit revisions, and make sure that any module writing any data (like form entries, activity log, security modules, etc.) has retention schedule. Check the database manually: Check the size of tables and overhead using phpMyAdmin or querying the database directly, and optimize tables after large cleanup. And if your website really needs complicated filters – write this data to the storage designed for this purpose.

While I wrote this section, I wondered if it was possible to add a database overload check. And yes it is possible, I’ve written a little script that will show issues in Tools > Site Health. It’s open source and free at https://github.com/obviousdisaster/My-WordPress-Functions.

2. Plugins: it’s not the number, it’s the overlap

“Too many plugins” is the typical conclusion, but it is only partially true. Thirty good-quality plugins are smaller than one that is poorly designed. It’s not about the number; it’s about what goes on when there is nobody maintaining the list for years.

Here is how it happens in practice:

  • Two or three different plugins for the forms on the site, each of which is activated. One is from the original codebase, one is added by someone who did not know how to use the first one and the last is included in the theme. All load their own scripts and styles regardless of the fact that the form itself is on one particular page only.
  • Up to four different page builders installed on the same site. One is used for the home page, another is used to make the landing pages created by some marketing agency, a third one is included in the theme and the fourth one is tried out. These are the heaviest things that you can put on the WordPress site. Having four such builders means loading four different sets of front-end and admin resources and having your content in four different incompatible formats.
  • Multiple plugins for SEO, security or optimization purposes all subscribing to the same set of hooks and interfering with each other.

Nobody made a deliberate decision that it had to be like this. It happened over time. Each plugin did the trick for somebody on the day of installation, and no one ever bothered to check if it was still required.

Solution: conduct a plugin inventory audit asking about each one separately: what does it do, and isn’t there another piece of software doing the same thing? Choose one solution for one job. Use Query Monitor to find out what queries each plugin generates and how much load it creates. Make sure to clean up after each plugin you decide to take out.

3. Caching on top of caching on top of caching

This is the most surprising of all: websites that are slow precisely because a lot of effort has been made to speed them up.

It works as follows. First comes the installation of a caching plug-in. Next comes server-level caching from the host. Then Redis is implemented. Finally, as an ultimate step, the website is protected by Cloudflare, with its own caching and optimizations enabled. Each of these measures was supposed to contribute to the performance. Instead, they are conflicting with each other.

Of course, layered caching in itself is not necessarily bad. A persistent object cache such as Redis, a page cache, and a CDN at the edge all perform distinct functions, and a properly configured system uses them together. What becomes a problem is when multiple layers try to perform the same function, and no one knows which one is responsible for it:

  • Several layers caching entire pages with different expirations and purging mechanisms. Update the page and hope to win the lottery on which page will be delivered to the user.
  • CSS and JavaScript being minimized, optimized in some way by the plugin and by the CDN, leading to script errors which are hell to debug.
  • Cache purges that purge only the first layer leaving the rest of layers to deliver stale content, and the client calling up and saying “I have updated it but nothing happened!”
  • Cache that is warmed up so infrequently due to something breaking it that the users see the uncached content anyway.

The effect of this is that you end up with a website that’s slower and more challenging to debug than without any caching at all.

Fix: be explicit about the responsibility of each layer. For example: Redis for the object cache, one page cache layer (server-side or edge-side but not both competing), and the CDN for static assets and edge caching. And ensure that purging one purges all others. Less is better than more, when it comes to caching layers.

WooCommerce: the cache you think you have

We need to give online shops a separate section here because this is where caching usually silently fails. As soon as the user adds a product to their cart or logs into the shop, the cookies set by WooCommerce force most page caches to not render this content because the same cached page cannot include information on the user’s basket. After that, everything gets rendered without the use of a cache, including pages viewed by your most precious visitors – who are about to purchase something.

And finally, WooCommerce’s “cart fragments” feature employs an AJAX call to refresh the basket number. In many cases, it runs on every page regardless of caching status, and it is never cached.

The solution: Make sure you have the caching set up for WooCommerce and exclude from it only what is necessary (basket, checkout, and account pages). Use cart fragments only when the mini-basket is active on a page. And since there will always be un-cached traffic to the shop, the database and code fixes discussed in this article become that much more important.

Cleared every cache? It might be their end

This is a common issue. You’ve made a change, they claim it’s not visible. So you clear the page cache, flush Redis, purge Cloudflare, and clear your own browser cache for good measure. You can see the change. They still can’t.

Before you start tearing the site apart, ask them this: is the issue happening in their office only? If you browse on 5G, does it show properly?

Many businesses have a firewall or web filtering appliance between their office network and the internet, often managed by a third-party IT company. Some of them cache web pages for everyone in the building, and some are configured to be very cautious about what gets refreshed. The result is that the whole office sees an old version of the site, while everyone else in the world sees the new one.

The quickest test is to ask them to check the site on their phone with wifi switched off. If it’s correct on mobile data and wrong in the office, the problem isn’t the website at all.

It’s bloody annoying to have to explain to a client that the reason they’re seeing an old version of their own site is their IT provider being overly paranoid, but it happens far more often than you’d think. It’s worth checking before you lose an afternoon to a cache you can’t control.

4. Custom code that time forgot

And the third major offender is bespoke coding: that theme or plugin created by someone ten years ago that’s been working just fine ever since and therefore hasn’t been touched once.

“Works” and “works efficiently” are two very different things. Some examples of the common mistakes are:

  • Querying database within a loop, where a page displaying 50 products executes 51 queries rather than one single.
  • Setting posts_per_page => -1 to retrieve all posts from the table. No problem if there are only 20 posts but a disaster if there are 20,000.
  • Using query_posts(), which has been deprecated for ages now and just wipes out the main query to rerun it.
  • Making calls to a remote API on the fly without any caching mechanism whatsoever, so everyone ends up waiting for someone else’s server response.
  • Not making use of object caching and having everything recalculated anew each time.
  • Coding in PHP versions that are outdated already, without profiling since then.

This is often totally sane on a site where there are only a few hundred posts and some moderate traffic. The size of the site increases but not the code.

And it’s almost never the product of a single programmer. An old, ten-year-old site could have the fingerprints of at least five developers: five different programmers who each solved the problem with five different methods and coding techniques, trying to get around previous codes written by others instead of using them. Since no one was ever given enough time to solve the original problem, everyone just kept on adding hacks on top of hacks until the architecture was built of patches.

The solution: profile before guessing. Query Monitor will point out which queries are being duplicated, which ones take too much time to run and which plugin or component is responsible for that. In most cases, the fix won’t take you more than half a day.

5. Hosting: the foundation nobody looks at

WordPress does not stand alone; it is part of a stack, usually LAMP: Linux, Apache, MySQL, and PHP. (Yes, it will run on Windows with IIS, but please don’t) Each and every component of this stack has an influence on the performance before WordPress does anything.

Web server. The classical one is Apache, and there is nothing wrong with that, but some hosting offer Nginx or LiteSpeed, which are much better at handling heavy loads.

PHP. Every new version of PHP gets noticeably faster than the previous ones, and using an outdated version is just losing a performance gain for free. OPcache, which keeps compiled PHP code in memory instead of compiling it anew each time it is requested, needs to be activated by default.

Database server. Untuned MySQL or MariaDB installation with not enough memory will have problems no matter how well-optimized your database is.

Server itself. None of the above means anything when the site is running on a crowded server along with hundreds of other sites.

PHP memory issues. When pages fail to load or show “There has been a critical error”, check the server error log for “Allowed memory size exhausted”. Compare the two numbers. Query Monitor shows the peak memory used to build each page. Site Health, found under Tools > Site Health > Info, kind of shows the server’s PHP memory limit under Server, and WordPress’s limits, such as WP_MEMORY_LIMIT and WP_MAX_MEMORY_LIMIT, under WordPress Constants. If pages are approaching WordPress’s limit, but the server allows more, raise it in wp-config.php; for example, define('WP_MEMORY_LIMIT', '512M');, up to your PHP limit. If you hit the server’s own limit, you can ask your host to increase it. But honestly, if a page needs nearly 512MB to build, raising the limit is a temporary fix: you’ve bigger problems; the rest of this article will help you find them. If it makes you feel any better, I’ve seen 2G and 4G in these settings!

No amount of optimisation within the WordPress environment will save a poorly built stack. Upgrading to the latest version of PHP and good hosting will be the greatest upgrade you can make and no code will have to be written for that.

Another irritating aspect is the WP-Cron which by default executes scheduled jobs during the actual visits of the users. This results in one unfortunate user having to wait during the process of making a backup, sending an email, etc. Changing this to a server cron is a matter of minutes.

And here is the harsh reality: few WordPress developers bother to dig deeper than WordPress itself. This isn’t a knock – many come from design backgrounds, many are on managed hosting services without server access, and control panels are there to help you never have to think about anything else. But that’s where some of the really great gains can be made. A developer can waste weeks tweaking performance improvements in a theme when the site is running an old version of PHP, without OPcache enabled and its database server is memory-starved. You cannot solve problems you cannot see, and for most WordPress users and developers, the stack is opaque. If you develop or manage WordPress sites, understanding the layers below it will make you a better developer than any plugin could.

6. The front end: where visitors actually feel it

All of this impacts the speed of the response. The actual experience of visitors and the metrics measured by Google’s Core Web Vitals are greatly impacted by the front end:

  • The bloated page builders and “all-in-one” themes that send a lot of CSS and JavaScript on every single page
  • Images uploaded directly from the camera that weigh 5MB apiece
  • Blockers for rendering
  • Third-party tags, like the chat widgets, the analytics, heat maps, ad scripts and social embeds, which cost time and many of which are no longer in use

Take any average slow WordPress website through Lighthouse and PageSpeed Insights, and third-party scripts are one of the major issues listed.

Images: stop uploading last century’s formats

Images will generally weigh down your pages the most, and are also the easiest to fix.

Do not use PNGs. Do not use JPEGs. Do not even think about using GIFs. GIF was invented in 1987. It is old school. Like literally.

Go with WebP. This is an image format that does everything that both JPEGs (images) and PNGs (transparency and sharper images) do at a smaller file size, can be used on all modern browsers, and has been supported by WordPress out-of-the-box since version 5.8. You could even push things further and use AVIF. For images where you need vector graphics like logos or icons, go with SVG but make sure to use them carefully as they are blocked by default by WordPress due to security reasons.

And, oh my lord, if you absolutely must have that Elmo GIF, make sure it is saved as WebM or AVIF. An animated GIF is an incredibly inefficient format for storing videos. The exact same video in WebM is usually much smaller. Stick it into a <video> element with autoplay, loop, muted and playsinline attributes enabled and it functions identically to a GIF without sucking down megabytes of data.

This is only part of the solution, however. Size also matters:

  • Scale your images down to the actual size they’ll be displayed at. That 6000-pixel wide picture taken with your phone when displayed in a 600-pixel wide column is far too big.
  • Pre-compress your images before uploading or use software that will do so upon upload.
  • Take advantage of WordPress’ built-in lazy loading capabilities and let the content management system load images when they come into view after scrolling.

Fonts and icons

Fonts on websites also constitute silent killers. An average culprit here includes loading four-five weights of Google Fonts (light, regular, medium, bold, and bold italic) while using only two, as well as an entire icon library with thousands of icons to just show a mobile phone, an envelope, and a hamburger icon.

The solution: Load only those font weights that you need, keep your fonts local, don’t use the service of a third-party font provider, and add the property font-display: swap so that text is rendered immediately and not after the font is loaded. For icons, include only those SVGs that you require and don’t include an entire library.

Third-party scripts

They are worth more than just a bullet point. Each chat widget, each tracking pixel, each heatmap, cookie banner, and social plug-in that is being added to your website is a third-party code that downloads from some third-party server and you have zero control over its weight. Even worse, they are being added as plugins; the heatmap from the marketing campaign three years ago that no one ever checked is still on your website.

The solution: create a list of all third-party scripts on your website and find out who uses them. Delete those that no one is using. For the other ones, load them only after the page has finished rendering or after the user has engaged with the page.

7. The admin area: slow for the people who pay you

Clients usually do not complain that the site is slow. Clients usually complain that editing the site is slow. The dashboard is sluggish, saving a post takes an eternity, and the editor is sluggish. This makes a difference since the admin area is what the client will use.

Typical culprits:

  • Heartbeat API: Pinging the server periodically while the admin is open. It helps with the auto-save feature and locking posts. But when you have several tabs opened on a weak server, this can be the culprit.
  • Plugins that phone home. License checks, updates, news feeds, and upsells. Some of them make remote calls each time an admin screen loads.
  • Widgets in the dashboard that perform costly queries and remote requests each time the user logs in.
  • Admin screens showing thousands of items, particularly orders or submissions from forms. Your inbox of Flamingo of 3 million rows of meta does not just slow down the front-end.

Solution: Use Query Monitor in the backend, as well as in the frontend. This tool will provide you with all necessary data regarding slow queries and remote HTTP requests right in the backend. Instead of disabling Heartbeat API, slow down its work; disable dashboard widgets that are not used; handle plugins with remote requests.

8. Where to start: find out what’s actually slow

Don’t assume. All of the solutions below are only useful if they target the specific issue your website is experiencing, so test before you try to fix anything.

  1. Is the site slow to load or just slow to serve the content? Test the performance with the help of PageSpeed Insights or Lighthouse. In case Time To First Byte is long, this means that the issue is in the back-end: sections 1-5 and 7. Otherwise, the performance issue is in the front-end: section 6.
  2. Test the technology stack used by the website. Which PHP version does the site use? Does the site have an OPcache? Is there a persistent object cache? What else is running on the webserver? Five minutes spent here may be a solution to several-day-long headache elsewhere.
  3. Install Query Monitor plugin. Take a look at the number of queries, slow queries, duplicate queries, HTTP requests and their source plugin or theme. Don’t forget to check the backend performance.
  4. Directly examine the database. Inspect table size and overhead from phpMyAdmin or by querying the information_schema.tables table. Inspect the total size of autoloaded options. If wp_postmeta and wp_options are way out of proportion to the content of the site, you’ve hit paydirt.
  5. Audit the plugins. One question for each plugin: What is its purpose, and what other plugins do the same thing?
  6. Audit the caching layers. List all of the caching layers that exist and what they cache. Also, list how they are cleared. If you don’t know the answers to these questions, neither does the site.
  7. Audit the front-end. Image formats and sizes, fonts, and all third-party scripts.

Address the largest issue first and test once more. You’d be amazed at how frequently it is simply one fix, such as upgrading PHP, cleaning up the database, or eliminating the duplicate page builder.

A word about scores: use them with caution

Run Lighthouse multiple times on the same page and you will see a range of different results. Run it on desktop and mobile devices and you will get dramatically different results. There’s nothing wrong here. The mobile audit is designed to simulate a slower connection and mid-level mobile device, and Google itself states that results vary each time due to network conditions and third-party software installed on your machine.

The problem is that the Lighthouse score is not what Google uses when ranking your website. This score comes from a lab test. In fact, Google evaluates Core Web Vitals based on actual traffic, something that you can measure in Search Console or the “Real Users” tab of PageSpeed Insights. Still, Google has made it clear that speed is only one of many factors: the more relevant page will beat a faster page.

Use those figures with care, then. If your site loads in a couple of seconds for actual users including a user on a mobile phone with a dodgy signal and not just you through fiber optic cable, forget about the score. The difference between a Lighthouse score of 78 and 85 does not affect your visitors, and chasing that difference will take up your valuable hours in a useless pursuit of diminishing returns.

There is plenty of good advice in that rabbit hole. Google for any WordPress performance problem, and you’ll come across forum threads and Stack Overflow solutions, often posted ten years ago for versions of WordPress, PHP, and browsers that no longer exist. Some of that information was relevant back then. Some of that information will make your site completely dysfunctional now. You won’t know unless you’re smart enough to figure out what matters and what doesn’t. Copying and pasting from older threads is how a slow site becomes both a slow and a dysfunctional one.

Fix the actual experience of real people: a 5 to 10 seconds loading page. Ignore the number.

So, is WordPress slow?

WordPress powers many of the largest and most active websites out there. Out of the box, on decent hosting, with a slim theme, it’s lightning fast.

What makes WordPress slow is years’ worth of decisions that weren’t looked back at: the second contact form, the fourth page builder, the three caches fighting each other, the loop that was written in 2014, the 3,000,000 rows of submissions of the form no one could access.

Nothing is inevitable here, and all but the smallest part of it can be sorted out without having to rewrite any of the code.

It’s worth keeping in mind when someone tries to sell you on starting again from scratch and having AI create a website for you. Sure, a static website is fast. But a well-optimized WordPress website with the page caching enabled is already delivering prebuilt HTML to your users, and it’s equally fast. And you keep everything that a CMS is supposed to offer: editability of content without involving developers, user roles, scheduled posts, revisions, and two decades’ worth of ecosystem built around it. Speed was never the excuse for abandoning WordPress. It was an excuse for taking care of it.

Just to be clear, I am not a blind cheerleader either. There is a lot about WordPress that pisses me off after two decades. Its media library is appalling, its editorial process appalling and block editor… Well, let’s just leave that alone. But “WordPress is slow” isn’t one of them.

The thing about a car that you haven’t serviced in ten years, don’t change the oil in, and ignore the warning lights is that it won’t work properly. No one ever says the manufacturer is to blame for that. It is the exact same situation with WordPress. All the slow sites are that way because of the neglect, not because of WordPress itself.

Whenever you hear some person whining about how slow WordPress is on LinkedIn or in a blog post somewhere, pay attention to what he/she is selling. It’s always an agency selling a headless rebuild, a new platform promising to replace your CMS using AI or a consultant offering something better. The actual truth is often much less interesting: a neglected website.

None of this is exciting. Nobody will thank you for pulling an all-nighter fixing vulnerabilities on fifty clients’ websites when a zero-day attack happens. Nobody will appreciate that you managed to strip down three million rows from your database or that you deleted a page builder no one uses. And nobody cares less about the super cool new plugin you’ve created in comparison to all that.

Because it all comes down to maintenance. No one sees all the CSS animations that someone worked on for so long as the page takes ten seconds to load and visitor leaves after three. Or because it got hacked. Or because it simply went down. The entire cool, creative work on the website is based on all that mundane work under the hood.

WordPress doesn’t have to be slow. It just needs someone to look at it.

And as for the “WordPress is dead”, I’ll deal with that lot another time.


So this is the bit where I try to sell you something?

Not quite. The reason I learned coding is because it was fun. The reason I learned WordPress is because it was far superior to anything I could have coded myself at the time. And the reason I learned server administration is because no one else was willing, bothered, or sufficiently terrified to do it. Somehow along the way, I’ve learned how to peel back the covers on just about everything in this article and then some.

A slow website may have only one symptom but a thousand and one possible causes, and figuring out which one is the real one is something that requires experience. And, honestly, this was always one of my favourite parts of the job. There was something wonderfully satisfying about putting on the metaphorical deerstalker, pretending I was Sherlock Holmes, and following the clues through the logs, database queries, PHP, JavaScript, server configuration and whatever else happened to be misbehaving. More often than not, I probably looked less like Sherlock Holmes and more like Inspector Clouseau, blundering around the crime scene while accidentally finding the culprit.

Me after reading a support ticket last week about a slow web site

But then you find it. That one stupid line of code. That plugin doing something it absolutely shouldn’t. That database query taking three seconds when it should take three milliseconds. That server configuration nobody has touched since the Bronze Age. You fix it, reload the page, and suddenly everything is fast again.

That feeling never gets old. You can’t beat it.


This took a week to write, because every time I thought I’d finished, I remembered another client site. Like plugins, the sections kept piling up and I never deactivated any of them.


AI Assisted Badge
Featured image is AI-generated. AI also helped shape the writing. The experience, opinions and war stories are all mine. I have the scars to prove it.

Comments (0)

Leave a Reply

Back To Top