There’s a moment every WooCommerce store goes through. It’s around €500k in annual revenue, or a few thousand active products, or a high-traffic campaign that breaks something nobody noticed in development.
The store stops feeling fast. Cart pages take three seconds. Checkout abandons spike. The host says “you need more resources” and quotes you a bigger plan. PageSpeed Insights paints everything red.
More resources rarely fix the underlying problem. What does fix it: knowing where WooCommerce specifically gets slow at scale, and which seven things are almost always the actual culprits.
The pattern
A small WooCommerce store is mostly cacheable. Visitors land on a category page (cached), browse products (cached), add to cart (uncacheable from here on), check out, leave. The cached pages cover 90%+ of traffic and the uncacheable cart/checkout flow handles a small minority of requests fast enough.
At scale, two things change. First, the catalogue grows — more products, more variations, more taxonomy terms, larger queries. Second, the share of cart/checkout traffic grows because conversion rates are roughly stable and you’ve added zeros to the visitor count. The uncacheable portion of your site becomes the bottleneck.
Here’s what actually breaks, in roughly the order I find it on audits.
1. Autoloaded wp_options bloat
Every WordPress request loads every option marked autoload = yes in wp_options into memory. On a default WordPress, this is maybe 200 KB. On a five-year-old WooCommerce store with a dozen retired plugins that never cleaned up after themselves, it can be 30 MB.
Check it with this SQL query:
SELECT option_name, LENGTH(option_value)
FROM wp_options
WHERE autoload = 'yes'
ORDER BY LENGTH(option_value) DESC
LIMIT 30;
If anything in the top 30 is bigger than 100 KB and you don’t know why it’s autoloaded, it almost certainly shouldn’t be. Common culprits: transient leftovers that never expired, cron job logs, third-party API caches, abandoned plugin migration data.
Switching the worst offenders to autoload = no (or deleting them entirely) typically shaves 100 to 300 ms off every uncached page. Single biggest performance win on most WooCommerce audits.
2. The wp_postmeta query disaster
WooCommerce stores almost all product data — price, stock, attributes, dimensions, variations — in wp_postmeta. A product with 12 variations and 8 attributes creates around 100 wp_postmeta rows. A catalogue of 500 such products is 50,000 rows. A catalogue of 5,000 products is half a million.
The default WordPress index on wp_postmeta is on (post_id, meta_key). It covers the “give me this product’s meta” pattern. It does not cover the WooCommerce-typical “give me all products where _price is between X and Y”, which forces full table scans.
WooCommerce 8.2 introduced custom order tables (HPOS), which moves order data out of wp_postmeta entirely. Enable it. For product data, the relief comes from either:
- An additional index:
ALTER TABLE wp_postmeta ADD INDEX (meta_key(32), meta_value(32))— handle with care on huge tables, but transformative on category pages with price filters - Moving the hot meta keys (
_price,_stock_status,_sku) into a custom flat table queried by your category pages
3. Cart fragments and the AJAX overhead
WooCommerce updates the mini-cart on every page load via an AJAX call to ?wc-ajax=get_refreshed_fragments. On a cached page this means: page loads fast (from cache), then immediately fires an uncached AJAX request that runs the full WordPress bootstrap.
On a busy store this single call generates more uncached PHP work than every other request combined. On thin servers it’s the difference between handling 1,000 concurrent users and falling over at 200.
Two fixes:
- Disable fragments entirely on pages where the cart isn’t visible (most category pages, blog, content pages). One filter, ten lines of code.
- Move the cart count to client-side: read from a cookie set on add-to-cart, render the mini-cart only on the cart and checkout pages. Cuts the AJAX call to roughly zero.
4. Plugin sprawl on cart and checkout
The cart and checkout pages run uncached on every request. Every plugin that hooks into woocommerce_init, woocommerce_checkout_process, or woocommerce_cart_calculate_fees pays its full execution cost on these pages.
On a typical 3-year-old WooCommerce site I’ll find:
- 3 abandoned-cart plugins (one current, two left over from previous experiments)
- 2 different conversion-tracking integrations (one set up by marketing, one by the previous developer)
- A coupon-extension plugin that adds 5 queries per cart calculation
- A shipping calculator that hits a remote API on every page load
The plugin audit on these two pages alone — what’s installed, what’s still actively used, what can be replaced with native WooCommerce or a single line of theme code — usually cuts cart/checkout response time by 30 to 50%.
5. Object cache, properly configured
Page caching (Cloudflare, LiteSpeed, WP Rocket) helps the cacheable pages. The cart and checkout flow needs an object cache — Redis or Memcached — to keep the uncacheable pages fast.
Most stores either don’t have one, or have it installed but misconfigured: shared across multiple sites without proper key prefixing, undersized so the cache thrashes, or with WooCommerce-specific groups (wc_session_id, wc_cart) not actually being cached.
Confirm with wp_cache_get_stats() or by inspecting Redis directly. A working object cache shows hit rates above 90% on a busy store. Below that and you’re probably caching nothing useful.
6. Image regeneration debt
Every redesign changes the image sizes the theme requests. WooCommerce stores accumulate dozens of intermediate sizes per product image — 300×300 from the first theme, 400×400 from the second, 600×450 cropped from a campaign.
The originals are usually fine. The problem is the dozens of obsolete sizes still being served because the markup still references them, or worse, the original image being served at 4× the displayed size because the right size doesn’t exist.
Audit with wp media regenerate after standardising on a sensible image-size set in the theme. WebP delivery via an image CDN (Cloudinary, Bunny, or a CDN with automatic format negotiation) gets you another 30 to 60% off image weight without touching the originals.
7. Background processing for the heavy work
By default, WooCommerce sends order confirmation emails synchronously, during the checkout request. The customer waits while SMTP negotiates. If your CRM/ERP integration is also hooked into woocommerce_thankyou, the customer waits for that too. If anything in that chain times out, the order’s already been created but the customer sees an error.
Move everything that isn’t critical for the response into ActionScheduler (the queue system WooCommerce already includes):
- Confirmation and admin emails
- CRM, ERP, accounting integrations
- Inventory sync to other channels
- Analytics post-back to anywhere that isn’t the conversion pixel
Checkout response time drops from 4 to 8 seconds (typical) to under 1.5 seconds (achievable). And one slow third-party API stops being able to break the entire checkout flow.
The order of operations
If you’re staring at a slow store and don’t know where to start, that’s the order I’d run through:
- Autoloaded options audit (1 hour, biggest single win)
- Cart fragments (1 to 2 hours, immediate impact on perceived speed)
- Plugin audit on cart and checkout (4 to 8 hours, real measurable gain)
- Object cache (varies by host setup)
- Background processing for emails/integrations (8 to 16 hours)
- Image regeneration and CDN (parallel work, doesn’t block the rest)
- postmeta indexes (last, requires careful staging and rollback plan on big tables)
Together, on a store I’ve audited, these seven items have routinely cut LCP from 4-6s to under 2s, doubled the number of concurrent users the same hosting plan can handle, and dropped checkout abandonment by single-digit percentages — which on a €500k store is real revenue.
None of them are mysterious. They’re just the things that don’t show up in a generic “speed up WordPress” guide because they’re WooCommerce-specific.