Why Your WordPress Site Is Slow, and Which Part Is the Host
Five things make a WordPress site feel slow, and hosting is only one of them. Measure TTFB, query count, image weight and the waterfall before you spend.
Priya Raghunathan · @praghunathan
Frontend and accessibility
- Topic
- PERF
- Section
- Performance
- Published
- 16 OCT 2026
- Reading time
- 9 MIN
- Words
- 2371W
The most expensive advice on the internet is "move to a better host". It costs a migration, a new billing cycle and a weekend, and it is wrong about half the time. A site can be slow on a fast server, and I have watched people move a slow site to a faster one and find it slow again a week later.
Five things make a WordPress site slow: the time the server takes to answer, assets that block the first paint, images that are heavier than the box they sit in, database work done on every request, and scripts loaded from servers you do not control. Only the first of those is hosting, and the other four follow the site to whatever host you pick next. So the useful question is not which host is fastest. It is which of the five you actually have.
You can answer that with a terminal, a browser and one query, and this article shows the measurement for each cause. One number does not identify a cause, though. A slow page is slow for a reason you have to separate out, and the separating is most of the work.
Time to first byte separates the server's share from the page's
Time to first byte is the part of the load the server owns. At 200 milliseconds the server answered at roughly the speed of a local handshake and everything after it is the document's problem. At two seconds nothing the page contains can explain the wait, because the page has not started arriving.
curl -sS -o /dev/null \
-w 'dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
https://example.com/
Run that five times over. The script prints seconds with millisecond resolution, so
ttfb=1.483 is a second and a half before the first byte arrived and
total=1.612 says the document then took 130 milliseconds. That is a server
problem. Reverse the two and the server answered in a quarter of a second while the page
spent two seconds assembling itself, which is the more common shape and the one a new host
does nothing about.
Two discipline points matter more than the command. Measure a warm page, meaning the second and later requests with whatever caches the site already runs at their normal state; a cold request rebuilds object and page cache entries and always looks worse. And do not compare a home connection with a phone tether on the same afternoon. The WordPress performance course on this site opens with exactly this problem, and its lesson on building a benchmark that repeats is worth reading before you trust a single curl run. My addition is narrower: if warm TTFB is under about 700 milliseconds, stop looking at hosting and look at what is on the page.
A slow page is usually a query count, not a slow query
Open the same page with a query counter and you find a different shape of problem. A default WordPress install serving a single post runs fifteen to thirty queries after a warm start. A page builder with eleven plugins runs three or four times that, and the database then spends longer deciding what to read than reading it. None of that shows in TTFB alone, because the queries run inside the request.
Use Query Monitor on a staging copy. On production the recording has a real cost, so gate it on an environment variable rather than shipping it to visitors:
// wp-config.php, staging only. SAVEQUERIES holds every query in memory
// until the request ends, so it belongs on a copy or behind a flag.
define( 'SAVEQUERIES', true );
// mu-plugins/slow-page-report.php
add_action( 'shutdown', function () {
if ( ! defined( 'SAVEQUERIES' ) || ! SAVEQUERIES ) {
return;
}
if ( ! isset( $_GET['slow-report'] ) || ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
$slow = array_filter( $wpdb->queries, function ( $row ) {
return $row[1] > 0.05; // Anything over 50 ms.
} );
error_log( sprintf(
'slow-report: %d queries total, %d over 50 ms',
count( $wpdb->queries ),
count( $slow )
) );
}, 999 );
The number to write down is the total. The number that matters is the difference between the page served warm and the same page served cold. If a cached page drops from 140 queries to 25 and stays slow, the database is not the bottleneck and you should look at TTFB or the frontend. If it drops and gets fast, you have found the work, and the course's query budgets lesson covers which queries to attack. Chasing the largest number in the list is the common mistake: a query that runs once costs less than one that runs forty times.
One more measurement belongs here. An object cache backed by Redis is only as good as its hit rate, and a Redis instance evicted down to nothing behaves like no cache at all. If your host sells an object cache and the query count is still high on every request, check the hit rate before concluding the cache is slow.
| Cause | What you see | What to measure |
|---|---|---|
| Server response | TTFB high, everything else normal | curl TTFB on a warm page |
| Blocking assets | Blank screen, then everything at once | Requests before first paint |
| Images | Fast start, slow finish | Bytes in uploads versus bytes displayed |
| Database | TTFB high, responsive host | Queries per request, warm and cold |
| Third parties | Fast server, long LCP | Requests to domains you do not own |
The image audit is a query you can run in an afternoon
Images are where WordPress sites store weight without noticing. The library keeps the upload and generates three or four smaller versions, so a theme printing one of those in a 300 pixel slot pays only for the small file. The same site printing the original in a 1200 pixel hero pays four megabytes. The audit compares the bytes you ship with the box you ship them into.
Run this on a copy of the database, not the live one, and treat the result as a candidate list rather than a verdict:
SELECT p.ID, p.post_title,
MAX(CASE WHEN pm.meta_key = '_wp_attached_file'
THEN pm.meta_value END) AS file,
MAX(CASE WHEN pm.meta_key = '_wp_attachment_metadata'
THEN LENGTH( pm.meta_value) END) AS metadata_bytes
FROM wp_posts AS p
JOIN wp_postmeta AS pm ON pm.post_id = p.ID
WHERE p.post_type = 'attachment'
AND p.post_mime_type LIKE 'image/%'
AND p.post_mime_type NOT LIKE 'image/svg%'
GROUP BY p.ID
HAVING metadata_bytes > 2000
ORDER BY metadata_bytes DESC
LIMIT 50;
Long metadata means many generated sizes and a deep srcset, which is normal
on a modern upload, and it tells you nothing about bytes on the wire. The bytes are on disk,
so list the files there as well: the layout does not care how many sizes exist, only which
one the browser chose.
cd /path/to/wp-content/uploads
find . -type f \( -name '*.jpg' -o -name '*.png' -o -name '*.webp' \) \
-size +300k -printf '%s\t%p\n' | sort -rn | head -20
Twenty files over 300 KB is a Tuesday afternoon, not a project. The audit does not
settle whether you are sending the right one. WordPress generates srcset and
sizes from a guess about your layout, and a wrong sizes attribute
buys the next width up on every phone. The course's lesson on
image
pipelines and responsive markup covers that, including the hero that should not be lazy
loaded and the one image that should carry fetchpriority="high".
Render blocking is a waterfall question, and the waterfall answers it in one look
The fourth cause shows up as a blank screen followed by everything at once: the browser waiting on stylesheets and scripts in the head before it paints anything. It is a decision the page makes about itself, and hosting has no influence on it beyond the speed at which those files arrive. A site that fixes its blocking assets gets faster on the host it already has.
Open Chrome DevTools, go to the Network panel, filter to CSS and JS, and look for requests whose response must be parsed before the first paint. Then run the same URL through WebPageTest and read the waterfall against the LCP marker: whatever is loading while the largest element is still missing is a candidate. Count the CSS requests that arrive before the hero text appears. Three or more, with none of them deliberate, is your page's problem, and it is a dequeue decision rather than a purchase.
Where the waterfall stops being decisive
A waterfall shows the requests, not their author, so two sites can show identical waterfalls and have different causes. Test the theory by dequeuing one stylesheet on a staging copy and re-running the URL. If the page looks the same and paints sooner, you have the answer; if it looks broken, you found a dependency rather than a cause.
Count third-party domains, then decide which ones have to stay
The fifth cause is the one a host cannot fix even in principle. Every request to a domain you do not own is a request whose speed you did not choose, and a chat widget in the head can hold the page longer than the rest of the document. The measurement is the count: sort the waterfall by domain and look at the hosts that are not yours. On a marketing site I would expect analytics, a tag manager, a consent tool, one or two fonts and possibly an A/B testing script. Anything past that is worth a question.
Then find out who asked for it. The registered handles on a page are readable from
WordPress with a short snippet, and the prefix usually names the author:
woocommerce_, wp-block-, or the plugin's own slug. For anything
you cannot place, look for the handle in the plugin's source before disabling it, because
a script half the checkout depends on does not announce itself.
The honest limit is that a fast site is not automatically a site you want. Analytics and consent scripts are not performance features, and removing them is a business decision with a compliance answer attached. Count the domains, keep the ones with a purpose, and measure each removal on its own.
Only one of these five is worth changing hosts for
Put the measurements beside each other and the decision becomes arithmetic. Warm TTFB above roughly 1.5 seconds with a low query count and a clean error log points at the server, and that is the case where moving is rational. With TTFB under 700 milliseconds and a long waterfall, the site is the problem and it stays the problem on any plan you buy.
Some links on this page are affiliate links. If you sign up through one, WPSifero may earn a commission. It does not change what is recommended here, and no provider has seen this article before you did. The tools named in the measurement sections are ordinary links or plain mentions: WPSifero has no affiliate relationship with any of them.
Where hosting is genuinely the answer, the vendor this site links to is SiteGround. Its three shared plans are StartUp, GrowBig and GoGeek. StartUp lists at $2.99 a month for the first twelve months, prepaid, renewing at $17.99, with one website and 10 GB of space; GrowBig lists at $4.99 and renews at $29.99 with unlimited websites and 50 GB; GoGeek lists at $7.99 and renews at $44.99 with 100 GB. The visit figures beside them, 10,000, 100,000 and 400,000 a month, are guides rather than caps. Each plan carries the same core set, so the tiers differ by capacity rather than features: a free domain for a year, an SSL certificate, email accounts, a CDN, daily backups and SiteGround's own caching. The entry price is a first-term discount and the renewal figure is the one that lasts. SiteGround's shared hosting page shows the renewal price directly beside the introductory one, so check both numbers before you decide, and check them again in a year.
Where SiteGround stops being right is worth stating as plainly as the price. It is shared hosting, so the account has a resource ceiling no visit number describes: a plugin-heavy WooCommerce catalogue or a post that reaches a large aggregator can push CPU and concurrent processes past what the plan allows, and the symptom is slow admin screens followed by a suspension notice. If you need root access, a specific PHP extension, or a server-level firewall you configured yourself, this is not the product. If your email is the part your business runs on, note that SiteGround includes mailboxes, so a future migration carries them too; keeping mail with a provider you would keep anyway makes the next move cheaper. If your traffic arrives in bursts you cannot predict, a fixed visit guide and a renewal price fit worse than an hourly plan. Those are not arguments against the product. They are the conditions under which it is the wrong one.
What to do on Monday
- Run the curl line five times against a page your visitors actually load, on a second request rather than a first, and note the median TTFB. That number decides whether the rest of this article applies to you.
- If TTFB is under 700 milliseconds, install Query Monitor on a staging copy and record the query count for that page warm and cold. If a cached page is still slow, the database is doing work the cache should have absorbed.
- Run the two audit commands against a copy of the uploads directory and the database. Fix the twenty largest files before anyone touches a plan.
- Open PageSpeed Insights or WebPageTest and count the requests that arrive before the first paint, then count the domains you do not own. Those two counts answer the question this article opened with.
- Only if the first four measurements are clean, price a host. Check the renewal price rather than the entry price, and check your peak month's visits against the plan guide rather than your average.
If the measurements come back clean, the next useful reading is not a pricing page. The WordPress performance and scaling course takes each cause from a number to a fix, and its lesson on lab data versus field data explains why your browser is the least reliable witness you have.
Priya Raghunathan · @praghunathan
Frontend and accessibility
Reviews interfaces for what happens when JavaScript fails, the keyboard is the only input, and the connection is slow. Focused on Core Web Vitals, semantic markup and progressive enhancement.
Continue