A brand-new WooCommerce store with twenty products and a clean theme is usually quick. Two years later, with thousands of products, tens of thousands of orders, a dozen extensions and a marketing stack bolted onto every page, the same store can feel like it is wading through mud — especially at checkout, which is exactly where speed matters most. The good news is that WooCommerce scales well when each layer of the stack is doing its job. This guide walks through those layers in the order we tackle them on real stores: measurement, caching, the database, background jobs, front-end weight, catalog search and, finally, the checkout itself.
Why WooCommerce stores slow down as they grow#
Most performance problems in WooCommerce are not caused by WooCommerce itself, but by how stores accumulate data and code over time. Understanding the three main forces makes the fixes much easier to prioritize.
Personalized pages cannot be cached like a blog#
A content site can serve almost every request from a page cache. A store cannot: the cart, checkout and account pages are unique to each shopper, and once a visitor adds something to their cart, WooCommerce sets session cookies that should send them past the page cache for the rest of their visit. That means a growing share of your traffic hits PHP and MySQL directly — and that share is made up of your most valuable visitors, the ones about to buy.
Data grows quietly#
Orders, order notes, customer sessions, scheduled actions, logs, transients and plugin settings all live in the database. None of them is a problem on day one. After a few years, tables that nobody looks at can hold millions of rows, and queries that were instant become slow, particularly in the admin and in reports.
Plugins accrete#
Each extension usually adds its own scripts, styles, database queries and sometimes remote API calls. Individually they look harmless; together they can double page weight and add hundreds of milliseconds of server time to every uncached request. Many load their assets on every page even if they only matter on one.
Measure before you optimize#
Guessing is the most expensive way to speed up a store. Before changing anything, establish a baseline you can compare against, and measure both the anonymous storefront and the logged-in, cart-carrying experience, because they behave very differently.
Field data first#
Start with real-user data. Google’s Core Web Vitals thresholds — Largest Contentful Paint at or under 2.5 seconds, Interaction to Next Paint at or under 200 milliseconds and Cumulative Layout Shift at or under 0.1, measured at the 75th percentile — are documented on web.dev. Search Console’s Core Web Vitals report and PageSpeed Insights show this field data when your store has enough traffic. Look at product pages and category pages separately; they usually have different bottlenecks. Our Core Web Vitals guide explains how to read these reports and what each metric is really telling you.
Server-side profiling#
Field data tells you that something is slow; server profiling tells you why. On a staging copy, a plugin such as Query Monitor shows slow and duplicate queries, hooks and HTTP API calls per request. Your host’s PHP slow log and MySQL slow query log, or an APM tool if you have one, reveal patterns across real traffic.
| What to measure | Where | What “bad” usually points to |
|---|---|---|
| TTFB for anonymous visitors | PageSpeed Insights, WebPageTest, curl | Page cache missing, misconfigured or frequently purged |
| TTFB with items in the cart | Browser devtools on staging | Slow PHP, heavy plugins, no object cache |
| Queries per request | Query Monitor | Plugins querying in loops, missing lookup tables |
| LCP on product pages | Search Console, PageSpeed Insights | Oversized gallery images, lazy-loaded hero image, render-blocking CSS |
| INP on product and checkout pages | Search Console, browser Performance panel | Heavy JavaScript, third-party scripts, variation logic |
| Admin page load times | Query Monitor in wp-admin | Autoloaded options, scheduled action backlog, postmeta-based orders |
Get the caching layers and hosting right#
Caching is not one thing. A healthy store uses several layers, each solving a different problem. Missing any one of them pushes load onto the layers beneath it.
Full-page cache with correct bypass rules#
Serve anonymous catalog traffic — home, category, tag and product pages — from a page cache at the server or CDN edge. The critical part is the bypass rules: never cache /cart/, /checkout/ or /my-account/, never cache requests that add to the cart, and skip the cache for any request carrying WooCommerce session, cart or login cookies. A simplified Nginx FastCGI cache configuration looks like this:
set $skip_cache 0;
# Never cache POST requests or URLs with query strings like ?add-to-cart=
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
# Personalized WooCommerce pages
if ($request_uri ~* "/(cart|checkout|my-account)/") { set $skip_cache 1; }
# Shoppers with a cart or session, and logged-in users
if ($http_cookie ~* "woocommerce_items_in_cart|wp_woocommerce_session_|wordpress_logged_in_") {
set $skip_cache 1;
}
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
Managed WordPress hosts and caching plugins usually ship WooCommerce-aware rules, but verify them rather than assuming: add a product to the cart in a private window and confirm the response headers show a cache bypass on subsequent pages.
Persistent object cache#
For every request that skips the page cache, a persistent object cache such as Redis or Memcached stores the results of repeated database lookups — options, product objects, term data — in memory between requests. On stores with many logged-in customers or heavy admin usage, this is often the single biggest server-side win. WordPress Site Health will even flag the absence of a persistent object cache on sites it judges large enough to benefit.
PHP version, OPcache and workers#
Run a currently supported PHP 8 release with OPcache enabled; each modern version has brought performance and language improvements, which we cover in our PHP 8 article. Also check how many PHP workers your plan provides. Because carts and checkouts bypass the cache, a sale or email campaign can exhaust a small worker pool quickly, and requests then queue rather than fail — which shows up as mysterious slowness during your busiest hours.
CDN and browser caching#
Static assets — images, CSS, JavaScript and fonts — should be served from a CDN with long cache lifetimes and fingerprinted filenames, so repeat visitors and later page views download almost nothing.
Keep the database lean#
A tidy database keeps uncached requests and the admin fast. These are the areas we check on every store.
Migrate orders to HPOS#
Historically, WooCommerce stored orders as posts with dozens of postmeta rows each, sharing tables with every page, product and revision on the site. High-Performance Order Storage moves orders into dedicated tables such as wc_orders, wc_order_addresses and wc_orders_meta, designed and indexed for order queries. According to the WooCommerce developer documentation, HPOS has been enabled by default for new installations since WooCommerce 8.2 (October 2023). Older stores can migrate from WooCommerce → Settings → Advanced → Features: enable compatibility mode so both storage systems stay in sync, confirm that your extensions declare compatibility, then switch the authoritative store to HPOS.
Audit autoloaded options#
Options marked for autoloading are loaded on every single request. Plugins sometimes store large blobs here — logs, caches, settings for features you no longer use — and the cost is paid everywhere. Since WordPress 6.6, Site Health warns when autoloaded data becomes large, and autoload values can be on, off, auto, auto-on or auto-off as well as the legacy yes/no. This query lists the biggest offenders:
SELECT option_name, LENGTH(option_value) AS bytes
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto')
ORDER BY bytes DESC
LIMIT 20;
Options belonging to plugins you have removed can usually be deleted; options for active plugins should be investigated before you change them, ideally on staging.
Expired transients, sessions and logs#
Transients are meant to be temporary cached values (see the Transients API documentation), but without a persistent object cache they live in wp_options and expired ones are only cleaned up periodically. WooCommerce’s own session table and log storage also grow with traffic. Make sure scheduled cleanup actually runs — which leads directly to background jobs.
Lookup tables#
WooCommerce maintains lookup tables, such as the product meta lookup table for price, stock and rating sorting and the attribute lookup table for layered navigation filters. If they fall out of sync after imports or direct database edits, queries fall back to slower paths or return wrong results. They can be regenerated from WooCommerce → Status → Tools.
- Take a full database backup and test it restores before deleting options or migrating order storage.
- Work on a staging copy first, especially for HPOS migration on stores with many orders.
- Never “optimize” by deleting rows you do not understand; orphaned-looking data is sometimes still used.
Tame background jobs and scheduled actions#
WooCommerce relies heavily on background processing: sending emails, syncing stock, processing webhooks, running subscription renewals, importing analytics data and cleaning up sessions. Two systems drive this work, and both deserve attention.
Replace traffic-triggered WP-Cron with a real cron job#
By default, WP-Cron checks for due tasks during normal page loads. On a quiet store, tasks run late; on a busy one, the checks add overhead to visitor requests. Disable the built-in trigger and run due events from the server’s scheduler instead:
// wp-config.php
define( 'DISABLE_WP_CRON', true );
# crontab: run due WordPress events every minute via WP-CLI
* * * * * cd /var/www/example && wp cron event run --due-now --quiet
Keep Action Scheduler healthy#
Action Scheduler is the queue WooCommerce and many extensions use for larger jobs. Check WooCommerce → Status → Scheduled Actions regularly. A growing number of pending or failed actions means something is stuck — often an extension calling an external API that times out. Completed actions are kept for a retention period (30 days by default) before being purged; on high-volume stores you can shorten it:
add_filter( 'action_scheduler_retention_period', function () {
return 7 * DAY_IN_SECONDS;
} );
For very large backlogs, WP-CLI’s wp action-scheduler run command lets you process the queue outside web requests rather than letting it compete with shoppers.
Control front-end weight on product and category pages#
Server speed gets the first byte out quickly, but the browser still has to download, parse and execute everything you send. On most stores we audit, front-end weight is where the largest Core Web Vitals gains are.
Images: right size, right format, right priority#
Product photography is usually the LCP element. Upload images at a sensible maximum size, let WordPress generate responsive srcset sizes, and serve modern formats such as WebP or AVIF (WordPress supports AVIF uploads since 6.5 when the server’s image library does). Never lazy-load the main product image or the first row of a category grid; WordPress adds fetchpriority="high" to the image it judges most likely to be the LCP element, but custom galleries and sliders often break that heuristic, so check the rendered HTML.
Load scripts only where they are used#
Slider libraries, form plugins, review widgets and chat tools commonly enqueue assets on every page. Dequeue them where they are not needed:
add_action( 'wp_enqueue_scripts', function () {
// The contact form only lives on the contact page.
if ( ! is_page( 'contact-us' ) ) {
wp_dequeue_script( 'contact-form-7' );
wp_dequeue_style( 'contact-form-7' );
}
}, 100 );
Script handles vary by plugin, so confirm them with Query Monitor’s “Scripts” panel before dequeuing. If you find yourself fighting a theme that ships megabytes of JavaScript, our article on modern JavaScript without the bloat explains how much interactivity you can achieve with very little code.
Cart fragments and mini-carts#
The legacy cart fragments script makes an AJAX request to refresh the mini-cart, and on uncached setups this request hits PHP on every page view. Recent WooCommerce versions no longer load it everywhere by default, but many themes and plugins re-enqueue it. If your header cart works without it — for example because you use the Mini-Cart block — remove it and test thoroughly.
Variable products#
When a product has more variations than the woocommerce_ajax_variation_threshold (30 by default), WooCommerce loads variation data on demand via AJAX instead of embedding it all in the page. Raising the threshold makes the swatches feel instant but bloats the HTML; lowering it does the opposite. For products with hundreds of variations, consider whether some attributes should really be separate products.
Catalog, filtering and search at scale#
Default WordPress search uses LIKE queries against post content. That is fine for a few hundred products, but relevance is poor and performance degrades with large catalogs. Stores with thousands of SKUs benefit from a dedicated search index — a hosted search service or a self-managed engine such as Elasticsearch or OpenSearch — which returns results in milliseconds and handles typos, synonyms and faceting.
Faceted filters deserve the same scrutiny. Filters that combine many attributes, price ranges and stock status can generate expensive queries, and every unique combination produces a URL that is unlikely to be cached. Keep filter combinations sensible, make sure the attribute lookup table is in use, and prevent search engines from crawling infinite filter permutations with sensible noindex and canonical rules. Pagination should use a fixed, reasonable number of products per page rather than “show all”.
Keep cart and checkout fast#
Checkout is uncached by definition, so every millisecond is real server work, and every heavy script directly hurts INP at the moment of purchase.
- Use the block-based Cart and Checkout. They have been the default for new installations since WooCommerce 8.3 and avoid many of the full-page reloads of the legacy shortcode checkout. Confirm your payment and shipping extensions support them first.
- Load payment scripts only on checkout. Some gateways inject their SDK site-wide to power express buttons. If you do not show express checkout on product pages, there is no reason to load it there.
- Audit third-party tags. Analytics, heatmaps, A/B testing and ad pixels compete with your checkout for the main thread. Remove what nobody looks at and defer what remains.
- Watch slow external calls. Real-time shipping rate APIs, tax services and fraud checks run during checkout. Cache rates where the provider allows it and set sensible timeouts.
- Reduce form friction. Fewer fields and correct
autocompleteattributes make checkout feel faster even when the server is unchanged — see our piece on UX principles that move conversion rates.
Finally, keep the store secure and updated: an outdated plugin is both a security and a performance liability, and our WordPress security hardening checklist covers the update and monitoring routine we recommend.
- Measure anonymous and cart-carrying visitors separately; they hit completely different code paths.
- Page cache the catalog, bypass it for anyone with a cart, and add a persistent object cache for everything else.
- Migrate to HPOS, audit autoloaded options and keep Action Scheduler and cron healthy.
- Most Core Web Vitals gains come from images and scripts on product and category pages.
- Treat checkout as sacred: minimal scripts, fast external calls, short forms.
Building a store that stays fast#
WooCommerce performance is not a one-off project; it is a habit. Stores that stay fast review their plugin list every quarter, check Scheduled Actions and autoloaded options during routine maintenance, test checkout after every significant update and keep an eye on field data rather than a single lab score. Each of those checks takes minutes, while recovering from years of neglect can take weeks.
If your store has slowed down as it has grown, our Performance & Technical SEO Audit pinpoints exactly where the time goes and gives you a prioritized fix list, and our WooCommerce Store Development service builds stores with these practices from day one. Get in touch and tell us about your store — we will tell you honestly where we think the biggest wins are.
Frequently asked questions
Can I use full-page caching on a WooCommerce store?
Yes, for anonymous visitors on catalog pages such as the home page, categories and product pages. Cart, checkout and account pages must never be cached, and any visitor carrying WooCommerce session or cart cookies should bypass the page cache so they always see their own cart.
What is HPOS and should I enable it?
High-Performance Order Storage moves orders out of the generic posts and postmeta tables into dedicated, indexed order tables. It has been the default for new WooCommerce installations since version 8.2, and older stores should migrate once their extensions are confirmed compatible, using the built-in sync and compatibility mode to switch safely.
Do I need Redis or Memcached for WooCommerce?
A persistent object cache is not strictly required, but it helps noticeably on stores with many logged-in customers, large catalogs or heavy admin use, because those requests cannot be served from the page cache. WordPress Site Health even recommends one when it detects a site that would benefit.
Why is my WooCommerce admin slow even though the storefront is fast?
The admin is never page-cached, so it exposes every slow query, oversized autoloaded option and overloaded background queue. Common culprits are a bloated Action Scheduler table, orders still stored in postmeta, analytics imports and plugins that run remote requests on every admin page load.
