The store was fast when it was small
When you launched, pages loaded quickly. Then the catalog grew to thousands of products, orders piled up, and plugins were added one by one. Now a category page takes seconds on a phone. Search in the admin hangs. The orders screen times out. On a busy evening, checkout slows down and some customers see an error.
Nothing broke on a single day, so it's hard to point at a cause. That is what makes this kind of slowness expensive: it grows quietly with the business, and the busiest days are the slowest.
Speed matters here in a plain way. A shopper waiting on a blank product page has time to go back to the search results. Paid traffic makes it sharper, because you pay for the click whether the page loads or not.
Field data first
Two kinds of numbers get mixed up:
- Field data comes from real Chrome users through the Chrome UX Report. Search Console's Core Web Vitals report shows it, over 28 days, judged at the 75th percentile.
- Lab data comes from Lighthouse: one simulated load. It is repeatable and good for finding causes, but it isn't what your customers felt.
I decide what to fix from field data and use the lab and the server to find out why. Google's guidance for Largest Contentful Paint is 2.5 seconds or less, Interaction to Next Paint 200 milliseconds or less, and Cumulative Layout Shift 0.1 or less.
Why it happens
The server takes longer for every page
Before the browser can do anything, the server builds the page. WooCommerce runs PHP and database queries on every uncached request. As the store grows, each of those costs more:
- Uncached pages. Cart, checkout and account pages can't be cached for everyone, and logged-in customers usually bypass the page cache entirely. A store with many returning customers serves a lot of uncached pages.
- Database queries. Product queries with many attributes, layered navigation filters, related products and "recently viewed" widgets get slower as the tables grow. Without indexes that match the queries, the database scans more rows every month.
- Autoloaded options. WordPress loads every autoloaded row in
wp_options on every request. Plugins leave large rows behind, including ones from plugins removed years ago.
- No object cache. Without Redis or a similar object cache, WordPress repeats the same queries on every request.
- Orders in the posts tables. Before HPOS, orders live in
wp_posts and wp_postmeta alongside products and pages. With a large order history, order queries and the admin slow down, and they compete with the storefront.
PHP workers run out
Each uncached request holds a PHP worker until it finishes. Hosting plans give a fixed number. When requests take longer, workers stay busy longer, and new requests queue. That is why a store can feel fine at noon and fall over in the evening: the work per request didn't change, but the queue did.
Cart fragments and admin-ajax
WooCommerce's cart fragments script calls ?wc-ajax=get_refreshed_fragments to update the mini cart. That request skips the page cache and runs PHP. Many themes still load it on every page, so a fully cached page still costs a worker per view. Plugins that poll admin-ajax.php for live stock, notifications or chat add more of the same.
The front end waits too
Even with a fast server, the page can feel slow:
- The main product image is lazy-loaded, oversized or set as a CSS background, so the browser finds it late.
- Every plugin adds its stylesheet and script to every page.
- Fonts in several weights load from a third-party host with no
font-display rule.
- Marketing tags stack up: the same pixel from a plugin and from Tag Manager, two copies of the GA4 tag, a chat widget that loads its full app on every view.
On a phone, each of these competes for a weak CPU and a slow network.
What fixing it involves
I work in a fixed order. Preloading an image is pointless if the server takes three seconds to send the HTML.
1. Measure
I pull field data for the key templates: home, category, product, cart, checkout. I run lab tests on a throttled mobile profile and record a trace for each. On the server, I profile slow requests with query logging and an application profiler, so each slow page gets a named cause: a plugin hook, a query, a remote API call.
2. Fix caching
Full-page caching for anonymous visitors, at the server or at the edge with Cloudflare, with cart, checkout and account pages excluded. An object cache. Cart fragments loaded only where a mini cart needs them. I check that a cached page never shows another customer's cart. A fast page that shows the wrong cart is worse than a slow one.
3. Fix the database
I clean up autoloaded options, remove tables and rows from plugins that are gone, add indexes where the profiler shows a scan, and replace slow plugin queries where that's the cause. For stores with a large order history I plan the move to HPOS: compatibility check on staging, sync mode, then the switch.
4. Size the server
With the slow code fixed, I look at the server again: enough PHP workers for the traffic, enough memory for the database and the object cache, current PHP and MySQL or MariaDB versions. If the host is the limit, I say so plainly and plan the move. See migrating off a slow host.
5. Fix the largest element and the scripts
Remove lazy-loading from the above-the-fold image, add fetchpriority="high", serve a correctly sized WebP or AVIF, and reserve its space. Remove duplicate pixels so each tag fires once, from one place. Defer what stays, and stop plugins from loading assets on pages where they do nothing. Fewer font weights, self-hosted and subset. I check tracking still works afterwards.
6. Guard against regressions
Speed gets lost one plugin at a time. I set a performance budget for each key template, monitor field data and server response time, and test plugin and theme updates on staging against the budget before they go live.
If the slowness shows up mainly when campaigns run, read scaling a store for ad traffic. This is the core of my performance engineering work.