Speeding Up a Slow WooCommerce Store

Speeding Up a Slow WooCommerce Store


A slow online store costs money in a way a slow blog doesn’t. Shoppers who wait for product pages browse less, and shoppers who wait at checkout sometimes don’t finish at all. Yet WooCommerce stores are often slower than the WordPress sites around them, even on decent hosting, and the usual “install a caching plugin” advice only gets you part of the way.

The reason is structural: a store has pages that can’t be cached the way a blog post can. In this guide I explain why WooCommerce stores slow down, and then go through the fixes I apply on real stores, roughly in order of impact. It builds on my general WordPress speed optimization checklist, so here I focus on what is specific to WooCommerce.

Why WooCommerce stores are slower

On a typical WordPress site, almost every page looks the same to every visitor, so a page cache can store the finished HTML and serve it without running PHP or touching the database. That’s why caching makes blogs so fast.

A store is different. The cart, the checkout and the customer’s account are personal: they depend on who is looking. Those pages, plus every “add to cart” request, must be built fresh by PHP and MySQL each time. They are also exactly the pages where speed matters most for revenue.

Store pages fall into two groups. Home, shop, category and product pages are the same for every visitor and can be served from a page cache. Cart, checkout, my account and AJAX requests such as add to cart are different for every shopper, so PHP and MySQL build them on every request.Same for every visitorHomeShopCategoryProductServed from the page cacheNo PHP, no database queries.Typically very fast. Cache thesepages aggressively.Different for every shopperCartCheckoutMy accountAdd-to-cart AJAXCart fragmentsBuilt by PHP + MySQL every timeCan't be page-cached, so speed heredepends on fast PHP, an efficientdatabase, an object cache and leanplugins. This is where stores lose time.
Figure 1: Page caching only helps the top row. A store’s most valuable pages sit in the bottom row, where every request does real work.

That gives us the strategy for the whole article: cache everything that can be cached, and make the uncached work as light as possible. The uncached work is PHP execution, database queries, AJAX requests and the plugins that hook into all of them.

Measure before you change anything

Guessing is the most expensive way to optimize. Before changing anything, I collect three kinds of numbers:

  • Real-user data from PageSpeed Insights (the “field data” section) for the homepage, a category page and a product page. This tells you what shoppers actually experience.
  • Server response time for the uncached pages. Add something to the cart, then time the cart and checkout pages in your browser’s Network tab. The first entry’s “waiting” time is roughly how long PHP and MySQL took.
  • What’s running on those pages, using the free Query Monitor plugin on a staging copy: number of database queries, slow queries, and which plugin is responsible for each.

Write the numbers down. After each change, measure again. Some fixes help a lot on one store and barely matter on another, and only measuring tells you which is which.

Caching done right for a store

Page cache, with the right exclusions

Use a page cache, whether that’s your host’s server-level cache or a well-known caching plugin, and make sure it excludes the cart, checkout and my-account pages, and bypasses the cache for visitors who have items in their cart. WooCommerce sets cookies such as woocommerce_items_in_cart and wp_woocommerce_session_* that good caching setups use for this. Most reputable caching tools handle WooCommerce automatically, but always verify: a cached checkout page that shows one customer’s details to another is a serious problem.

Persistent object cache

For the pages you can’t page-cache, a persistent object cache (usually Redis, sometimes Memcached) is one of the most effective upgrades. WordPress and WooCommerce cache the results of many database lookups in memory, so repeated queries for products, options and sessions don’t hit MySQL every time. Many managed hosts offer Redis with one click; on a VPS it’s a service plus a drop-in plugin. Check the result with:

# Should report "Redis" (or similar), not "Default"
wp cache type

Cart fragments and other AJAX requests

The classic WooCommerce mini-cart keeps itself up to date with the cart fragments script (wc-cart-fragments), which sends an uncached AJAX request (?wc-ajax=get_refreshed_fragments) to refresh the cart contents. On a busy store, a request like that on every page view adds a lot of uncached server work.

WooCommerce improved this in version 7.8: the script is no longer loaded on every page by default, only where it’s needed, such as when the classic Cart widget is used. However, some themes and plugins still enqueue it everywhere. Check your page source for cart-fragments. If it loads on pages that don’t need a live cart (blog posts, for example), you can remove it there:

<?php
/**
 * Don't load cart fragments on content pages that have no live mini-cart.
 * Test your header cart afterwards: if it must update everywhere, skip this.
 */
add_action(
	'wp_enqueue_scripts',
	function () {
		if ( ! function_exists( 'is_woocommerce' ) ) {
			return;
		}
		if ( is_woocommerce() || is_cart() || is_checkout() || is_account_page() ) {
			return;
		}
		wp_dequeue_script( 'wc-cart-fragments' );
	},
	20
);

An alternative is the Mini-Cart block, which doesn’t rely on the cart fragments API at all. Either way, test adding, updating and removing items afterwards, including from the cart page.

The WordPress Heartbeat API is another source of repeated requests. It powers autosave and post locking in the admin, but it also runs on some front-end pages. Slowing it down reduces background load:

<?php
// Run Heartbeat at most once a minute (the post editor defaults to every 15 seconds).
add_filter(
	'heartbeat_settings',
	function ( $settings ) {
		$settings['interval'] = 60;
		return $settings;
	}
);

Clean up the database

Stores accumulate data much faster than ordinary sites. Every order, every cart session and every background job leaves rows behind. Over a couple of years that slows down the queries that the uncached pages and the admin depend on. Start by finding out what is actually big:

# Largest tables first
wp db size --tables --human-readable --orderby=size --order=desc

# How many completed background jobs are being kept?
wp db query "SELECT status, COUNT(*) FROM wp_actionscheduler_actions GROUP BY status;"

(Replace wp_ with your own table prefix.) These are the usual suspects:

Four places where a WooCommerce database grows: Action Scheduler tables, customer sessions, expired transients in the options table, and orders stored in posts and postmeta. Each card lists the fix.Action Schedulerwp_actionscheduler_*Completed and failed backgroundjobs are kept for weeks.Fix: shorter retention periodCustomer sessionswp_woocommerce_sessionsOne row per active cart. Grows ifthe cleanup job never runs.Fix: make sure WP-Cron runsTransients and autoloadwp_optionsExpired caches and large autoloadedoptions load on every request.Fix: delete expired, trim autoloadOrders in posts tableswp_posts + wp_postmetaLegacy order storage mixes orderswith pages, posts and products.Fix: switch to HPOS
Figure 2: The four places where WooCommerce databases typically grow, and what to do about each one.

Action Scheduler

WooCommerce and many extensions use Action Scheduler for background jobs: emails, webhooks, subscription renewals, analytics imports. Completed actions are kept for 30 days by default. On a busy store that can mean hundreds of thousands of rows. You can shorten the retention period:

<?php
// Keep completed Action Scheduler jobs for 7 days instead of 30.
add_filter(
	'action_scheduler_retention_period',
	function () {
		return 7 * DAY_IN_SECONDS;
	}
);

Also look at WooCommerce → Status → Scheduled Actions for failed or past-due actions. A pile of past-due actions usually means WP-Cron isn’t running reliably. On a store, I prefer to disable WordPress’s visitor-triggered cron and run it from a real server cron job every minute, so background work doesn’t depend on traffic.

Transients, autoload and orders

Clear expired transients with wp transient delete --expired, and check for large autoloaded options (the query is in my Core Web Vitals checklist). For orders, the biggest structural improvement is High-Performance Order Storage (HPOS), which moves orders out of the posts tables into dedicated tables. New stores use it by default; older stores can switch once their extensions are compatible. I explain what that involves in WooCommerce HPOS: making your code compatible.

Product images and front-end assets

Product pages are image-heavy by nature, and the main product image is usually the page’s Largest Contentful Paint element. A few rules make a big difference:

  • Right-size the image settings. Under WooCommerce → Settings → Products (or the Customizer’s WooCommerce section, depending on your theme), set image widths that match your design. Uploading 4000-pixel photos and letting the browser shrink them is the most common waste I see.
  • Serve modern formats. WebP or AVIF versions of product photos are usually far smaller than JPEGs at the same visual quality.
  • Don’t lazy-load the main product image. Lazy-load gallery thumbnails and related products, but let the first image load immediately, ideally with fetchpriority="high".
  • Load shop assets only on shop pages. If your blog and landing pages don’t sell anything, they shouldn’t load WooCommerce’s styles.

WooCommerce lets you control its own stylesheets with a filter. This keeps them on store pages and removes them elsewhere:

<?php
// Only load WooCommerce's default styles on store-related pages.
add_filter(
	'woocommerce_enqueue_styles',
	function ( $styles ) {
		if ( is_woocommerce() || is_cart() || is_checkout() || is_account_page() ) {
			return $styles;
		}
		return array();
	}
);

If your homepage shows products (a featured products block, for example), keep the styles there too, and check every page type visually after making the change.

Plugins, admin speed and hosting

Audit plugins by cost, not by count

Twenty light plugins can be faster than three heavy ones. In Query Monitor, open the cart and checkout pages and look at “Queries by Component” and the slow-query list. Product filter plugins, page builders loading on every page, “related product” engines and analytics plugins that write to the database on each visit are frequent offenders. For each heavy plugin, ask: do we still need it, is there a lighter alternative, or can a few lines of custom code replace it? My article on when custom development makes sense covers that decision.

Don’t forget the admin

A slow orders screen costs your team time every day. HPOS, a persistent object cache and removing plugins that add heavy admin columns or dashboard widgets usually help most. WooCommerce Analytics imports historical data in the background via Action Scheduler; let it finish rather than repeatedly restarting it.

Hosting that fits a store

Because carts and checkouts can’t be cached, a store needs real server capacity: a current PHP version (PHP 8.x is noticeably faster than 7.x), enough PHP workers to handle simultaneous checkouts, a fast database and Redis. Cheap shared hosting that’s fine for a brochure site often struggles with a store during a sale. Measure server response time on the checkout; if it’s slow even after the fixes above, the server is the bottleneck.

What to expect

Every store is different, so I won’t promise specific numbers. To show the shape of a typical improvement, here is an illustrative example of how the key metrics move when the fixes above are applied to a store with a common set of problems: no object cache, cart fragments everywhere, a bloated database and oversized images.

Illustrative example, not measured data. Product page load falls from 3.2 to 1.4 seconds, checkout server response from 2.4 to 0.8 seconds, and page weight from 3.6 to 1.3 megabytes.BeforeAfterIllustrative example, not measured dataProduct page loadsecondsBefore: 3.2 sAfter: 1.4 s3.21.4Checkout responseseconds (server)Before: 2.4 sAfter: 0.8 s2.40.8Product page weightmegabytesBefore: 3.6 MBAfter: 1.3 MB3.61.3BeforeAfterBeforeAfterBeforeAfter
Figure 3: An illustrative example (not measured data) of how store metrics typically move after caching, database, AJAX and image fixes. Each panel has its own scale.

Notice that the checkout improves for different reasons than the product page: the product page benefits mostly from caching and images, while the checkout benefits from the object cache, a leaner database and fewer heavy plugins. That’s why a store needs both kinds of work.

FixHelps most withEffort
Page cache with correct exclusionsHome, shop, category, product pagesLow
Persistent object cache (Redis)Cart, checkout, adminLow to medium
Remove unneeded cart fragmentsServer load on content pagesLow
Database cleanup and HPOSCheckout, orders screen, reportsMedium
Image sizes and formatsProduct page LCP and weightLow to medium
Plugin auditEverything, especially uncached pagesMedium
Better hosting / newer PHPEvery uncached requestVaries
Where each fix pays off, and roughly how much work it is.

Key takeaways

  • Cart, checkout and account pages can’t be page-cached, so their speed depends on PHP, the database and plugins.
  • Cache everything else aggressively, and add a persistent object cache for the rest.
  • Remove cart fragments where no live mini-cart needs them, and slow down Heartbeat.
  • Keep the database lean: Action Scheduler retention, working cron, expired transients, HPOS.
  • Measure before and after every change, on the pages that make money.

If your store feels slow, especially at checkout, I can audit it, find where the time actually goes and fix it without breaking the shopping experience. Send me your store’s URL and a short description of what feels slow.

Code examples in this post are free to use under the MIT licence. Article text © 2026 Farooq Bin Munir.