How we fixed a slow WordPress site

'A mechanic lying under the front of a car in a workshop, with the engine bay stripped open and covered by a dust sheet.'

A business moved its website onto our hosting, and the promise at the point of sale was speed. After the migration the scores hadn’t improved much, and on a couple of measurements they’d gone the wrong way.

Which tells you something useful straight away. Hosting sets the floor a site performs from. It cannot undo what a site is doing to itself, and this one was doing a great deal. That’s the reason the investigation below runs as deep as it does, all the way down to which version of PHP the account was running: a site this bloated doesn’t have one problem, it has a stack of them, and you don’t get to skip any of them by moving the site somewhere faster.

Here’s the whole job, in order, with the numbers. It’s worth reading if you own a WordPress site that’s been alive for a few years.

The site itself was small. Fourteen pages, 180 blog posts, 120 podcast episodes. A UK professional services business with a modest amount of content and a very large amount of machinery underneath it: 37 active plugins, a 3.5GB media library, a 121.6MB database, 4.2GB of install. A site that size, built well, runs on four to six plugins.

Was it the platform or was it the site

The first job was to find out which, and it mattered commercially. If you’ve just sold hosting on a performance promise, you need to know quickly whether the platform can actually deliver it, or whether you’re about to spend a week inside somebody else’s website.

So we measured the server’s response time, the wait before a page even starts arriving, in three states.

On the new platform, with the cache serving, it was 0.085 seconds. On the new platform with the cache bypassed, it was 1.40 seconds. On the site’s previous host, 1.87 seconds.

There’s the answer. The cache was hiding a slow origin. Ordinary visitors landing on a cached page were fast, and everyone else paid the full 1.4 seconds: every uncached page, every logged-in session, every form submission, every search engine crawler hitting a cold path. And the origin was only 25% faster than where the site had come from. The visible improvement was almost entirely the cache doing its job in front of a server that hadn’t got any quicker.

It also explained the scores. Testing tools frequently hit uncached paths, so the lab was measuring the 1.4 seconds, not the 85 milliseconds.

The server was recompiling the site on every visit

The account was running the newest available version of PHP, the language WordPress is written in. On this particular hosting stack, that newest version profile had been built without an opcode cache. The version one step below it had one.

An opcode cache is unglamorous and it matters enormously. Without it, the server recompiles every source file from scratch on every single request. With 37 plugins loaded, that’s a substantial bill to pay several times a second.

We didn’t assume. We tested it, running the two versions against each other in an interleaved sequence, twelve samples per run.

PHP version Opcode cache Mean response Median Range
Newer Not loaded 1.291s 1.296s 1.180 to 1.404s
One step down Enabled 0.567s 0.559s 0.494 to 0.658s
Newer Not loaded 1.502s 1.491s 1.217 to 1.826s
One step down Enabled 0.468s 0.441s 0.418 to 0.602s

The ranges don’t overlap anywhere. The newer version’s fastest sample was slower than the older version’s slowest. Roughly 880 milliseconds recovered on every uncached request, from a configuration change, with the page output byte for byte identical.

A newer version number is not automatically a faster one. Check which extensions are actually loaded on the version you’re running, and check it with a probe rather than trusting the control panel.

One footnote worth having if you ever do this yourself. An opcode cache has to warm up, and it warms up per worker process, which the web server recycles. Our first twelve samples after the change averaged 1.358 seconds with a wild spread. After 25 warming requests it settled at 0.500 seconds. Don’t judge a change like this on the first few page loads.

What eight years of adding things had done

The site was built on a budget marketplace theme, still running at the version it was installed at, never updated. There was no child theme, so every customisation had been written directly into the theme itself, which meant it could never be safely updated even if someone wanted to. The theme’s author had long since moved on. It was frozen.

The theme also shipped commercial plugins under its own licence, including a page builder that 22 pages depended on. Those can’t be updated independently and can’t be transferred.

And because it was written before PHP 8, the error log filled continuously with compatibility warnings from the theme and its companions. At some point two diagnostic plugins had been installed to investigate that, and left running in production.

Then there’s the accumulation. The slider plugin held 22 sliders. One was in use. Thirteen were the untouched demo sliders that came with the theme. The rest were duplicates with names like “Main Hero Duplicate 2”. The theme bundled seven icon font sets and loaded all seven on every page, and a separate plugin loaded an eighth and a ninth from an external source. Two of the nine were referenced anywhere on the site.

Underneath that sat data belonging to software that wasn’t installed any more: 300MB of snapshots from a reset plugin, 767MB of archives from a backup plugin, 27MB of logs and 25 database tables from a security plugin, and 1,412 post revisions taking up 56MB because revisions had never been capped since 2018. The revisions table alone was bigger than every piece of real content on the site combined. A configuration file from a host two moves ago, dated 2018, was still being loaded on every request.

Scheduled tasks were still firing for a backup plugin, a code snippets plugin and a social feed plugin, none of which existed on the server. Those run on visitor page loads, and they were doing nothing at all.

The best single find was the slider. It was loading 187KB of code on every page of the site in order to render a page builder row that somebody had switched off, presumably when the hero section was rebuilt. Nobody had noticed, because the page looked right.

What we changed

We corrected the PHP version first and verified the output was identical with no fatal errors, before and after.

Then plugins, 37 down to 14. Every removal was checked against content, settings, widgets and database tables first, and nothing came out on an assumption. The booking plugin had zero shortcodes and a single appointment record dated 2019. A 77MB plugin suite had never been connected to the service it talks to, so every module inside it was inert. A popup plugin had no popups configured. Three social feed plugins had no shortcodes between them. Five developer diagnostic tools were running in production.

Then the data. Revisions deleted and capped at five going forward, 25 orphaned tables dropped, 21 unused sliders removed, around 1.4GB of abandoned files cleared, the dead scheduled tasks removed, and comments closed properly rather than through a plugin. The database went from 121.6MB to 40.4MB, with the posts table falling from 72.41MB to 5.92MB.

Then delivery, which produced the largest share of the front-end gain. Contact form assets now load only on pages that contain a form, and podcast player assets only on podcast content. The five unreferenced icon sets are gone. Four small theme stylesheets totalling 6.6KB were costing four separate round trips, measured at roughly 1,260 milliseconds on a throttled connection, so they’re now written directly into the page instead: the bytes were never the problem, the connections were. The two surviving icon stylesheets load without blocking, since nothing above the fold uses an icon. Fonts were told to show fallback text immediately, and the connections to the font hosts are opened early rather than half-opened.

Render-blocking stylesheets went from 31 to 3.

Last, the media. The library held roughly 10,300 image files, two of which were in a modern format. Thirteen videos were being served from the site itself, totalling around 540MB, the largest of them 63MB. The largest single image was 12MB, and seven were over 8MB. Camera originals had been uploaded straight into the CMS for about eight years with no compression step at any point. We compressed the library. The homepage hero image went from 308KB to 108KB with no visible difference.

The results

Before

After

SpeedVitals, Lighthouse 12.8.2, London UK, Desktop, HTTP/2, throttled
Before After
Grade D A
Performance score 45% 97%
Main content appears 3.8s 1.0s
First content appears 3.5s 0.8s
Page ready to use 5.3s 1.6s
Server response 2.2s 26ms
Page weight 3.9MB 1.2MB
Requests 132 68
Active plugins 37 14
Render-blocking stylesheets 31 3
Database size 121.6MB 40.4MB

Before

After

SpeedVitals, Lighthouse 12.8.2, London UK, Desktop, HTTP/2, throttled

The number we’d point a technical reader at isn’t any of those, though. It’s the shape of the wait. Before the work, server response accounted for 58% of the time it took the main content to appear. After it, 6%, with the remainder spread across loading and rendering. The bottleneck moved, and a bottleneck moving is what a successful optimisation actually looks like from the inside.

Third-party blocking time went from 149 milliseconds to zero, because the two culprits weren’t needed on the homepage at all.

Why none of this is only about speed

Everything above is also groundwork search engines read. A page that arrives quickly, can be crawled without obstruction and doesn’t make a phone wait on 31 blocking stylesheets is a page a search engine can work with, and the same clean-up that moved these numbers moved this site’s search performance with it. That’s why this sits in the same conversation as technical SEO rather than in a separate one about page speed.

It’s also the argument for doing it at the right moment. A website migration is the one point where a site’s entire structure, every URL, every asset, every plugin decision, is genuinely on the table. That makes it the cheapest time to fix all of this, and, handled carelessly, the most expensive time to ignore it.

What we could not fix, and why

Two files account for nearly everything that’s left, and both are structural.

The theme ships a 568KB stylesheet, of which 94.2% is never used on the page. The page builder ships 448KB, of which 99.3% is never used. Both block rendering. Neither can be trimmed without editing the theme, and the theme can’t be safely edited because there’s no child theme. That’s the ceiling. Going past it means replacing the theme and the page builder, which is a rebuild with a different budget and a different set of risks, not an optimisation.

One measurement also went the wrong way, and it stays in the numbers. Layout shift, how much the page jumps about as it loads, moved from 0.023 to 0.035. Both sit comfortably inside the good threshold, but it moved the wrong way and it’s worth saying why. It’s almost certainly the font change. Swapping from a fallback font to the real one causes a small reflow, and we took that trade knowingly: text lands on screen roughly 2.7 seconds earlier at the cost of a slight shift when the web font arrives. It can be tightened further by matching the fallback font’s metrics, which is a follow-up rather than something left broken.

Being straight about one metric moving the wrong way is what makes the other eleven believable.

What mattered more than speed

Two things surfaced during the work that had nothing to do with performance and were more serious than any of it.

Once security software was installed, its scanner flagged 15 documented vulnerabilities across the plugin set. The worst was a flaw allowing a file to be read from the server with no login of any kind, in a social feed plugin that had zero shortcodes anywhere on the site, had been quietly pulling content into unpublished drafts, and had been sitting there for years. A booking plugin carried four flaws including account takeover. The page builder carried seven, and because 22 pages depend on it, it had to be updated rather than removed.

Which leads to the distinction most site owners never hear. Deactivating a plugin is not removing it. Switching it off delivers the performance gain, because the assets stop loading. It does not take the code off the server, and for certain classes of flaw the files remain reachable. Deletion is what closes the exposure. “We turned it off” reads like a finished job and isn’t one.

Two other things were true. The analytics tag was hardcoded into a plugin settings field with no consent mechanism of any kind, so analytics cookies were being set on page load for every visitor, which under UK GDPR and PECR requires consent first. And administrator accounts from earlier arrangements were still active, alongside a plugin granting dashboard access from outside, while all 194 published items had been created under one shared login, so there was no attribution and no way to revoke one person’s access without changing everybody’s password.

Where to start with your own site

If any of the above sounded familiar, the useful first step isn’t a plugin. It’s finding out what’s actually happening on your site, in what order, and which of it is load-bearing. That’s what our free website teardown does: a plain read of what your site is costing you and what could realistically be recovered, reviewed by a person, back with you inside a working day. If the answer turns out to be structural rather than fixable, we’d rather tell you that than sell you a tune-up, and our build work and ongoing care both start from the same measurement.

Questions answered

Will moving to faster hosting fix a slow website?

It raises the floor, and it stops there. Hosting cannot undo what an application is doing to itself, and here the cache made cached pages fast while the origin stayed only 25% quicker than the old one. Measure the server response with the cache bypassed before you promise anyone anything, because a fast cached page hides a slow site completely.

How many plugins is too many?

There's no magic number, but the ratio tells you something. This site had a modest amount of content and 37 active plugins, where a well-built equivalent runs four to six. The real test isn't the count, it's whether each one is actually doing a job. Several here were loading code on every page while rendering nothing at all.

Is deactivating a plugin the same as removing it?

No, and the difference matters for security rather than speed. Deactivating stops the assets loading, so you get the performance benefit immediately, but the code stays on the server and for some vulnerabilities the files can still be reached. Deleting is what actually closes it. Treat deactivation as a temporary state and finish the job.

Can any WordPress site reach these numbers?

Not from optimisation alone. Here we got to a 97 performance score and then hit a hard ceiling, because a theme stylesheet with 94% unused code cannot be trimmed on a site with no child theme. Checking how much of the code is genuinely used is how you find out honestly, before quoting, whether a site needs tuning or rebuilding.

Method note: SpeedVitals with Lighthouse 12.8.2, London UK, Desktop, HTTP/2, throttling applied, identical configuration for the before and after tests. Server response comparisons used an interleaved A/B sequence with twelve samples per run and a warming period before measurement. Figures come from our own optimisation work on a client property, anonymised throughout.

Want this kind of thinking on your problem?

30 minutes, free, no slides. Two of us, one focused question, a few honest answers.

Book the call