So you Google "WooCommerce slow checkout." You get three kinds of results: general WordPress speed guides that don't mention checkout at all, WooCommerce hosting company posts that tell you to upgrade your plan, and plugin recommendation roundups that eventually say "install WP Rocket."
None of them are exactly wrong. They're just answering a question that isn't yours.
Quick disclosure since I always include one: I run AcceleratorWP, a plugin that handles checkout performance as part of a broader structural approach to WordPress speed. I'll mention it once at the end. Everything before that works without installing anything I sell.
What people Google, and what they actually want
The search query is usually one of these:
- "WooCommerce checkout page slow"
- "WooCommerce slow to place order"
- "WooCommerce checkout TTFB high"
- "WooCommerce checkout 3 seconds"
- "speed up WooCommerce checkout"
Underneath all of them is the same reading: the checkout page itself takes too long to load, and the normal speed advice — cache plugins, CDNs, image compression — hasn't moved it. That observation is correct. The checkout page does sit in its own performance category, separate from every other page on the site, and the gap is not a coincidence. It's structural.
Why checkout is a different beast
The standard approach to WordPress performance is caching: render the page once, store the result, serve the stored version to every subsequent visitor. WP Rocket, LiteSpeed Cache, NitroPack, your hosting company's built-in CDN layer — all of them work this way. And for a blog, a product listing, an About page, a homepage: it works. Extremely well.
Checkout cannot be cached. Not "can't easily be cached." Cannot. WooCommerce sends Cache-Control: no-cache, no-store, must-revalidate headers on the checkout page, and every serious cache plugin respects that and excludes the URL by default. WP Rocket's exclusion list ships with /checkout/ in it. LiteSpeed Cache excludes it. They do this intentionally because a cached checkout page is worse than a slow one: stale nonces break payment form submission, outdated inventory data lets customers order products you sold out three hours ago, old cart totals reflect a coupon that expired yesterday.
So when WP Rocket saves you 2 seconds on the homepage — it does nothing for checkout. The install is still worth it for the rest of the site. But for the checkout TTFB problem, it is the wrong tool. The roundups don't mention this because they're optimising for the pages where caching works. Which is most pages. Just not this one.
The follow-up question is: if caching is off the table, what can you actually do? The answer starts with understanding what checkout actually does on each request.
The three costs no one separates
Checkout TTFB comes from three distinct layers. They compound, and the highest-leverage fix sits at the first layer, which is also the one most guides skip entirely.
Boot cost. WordPress boots with every active plugin on every uncached request. Your checkout page has 38 active plugins? All 38 load, every time. Your SEO plugin registers its XML sitemap hooks. Your social sharing plugin registers its Open Graph meta. Your slider plugin loads its shortcode parser. Your newsletter popup loads its dismissal logic. Your WooCommerce store has maybe 10–12 plugins that have genuine work to do on checkout. The other 25–28 run because WordPress doesn't distinguish — it hands every plugin the same keys and lets them decide whether to do anything.
Run cost. After boot, WordPress executes the request: reconstructing the cart from the session, recalculating shipping rates, initialising every payment gateway, running tax logic, generating security nonces, firing woocommerce_checkout_fields filters across every plugin that registered one. On a 40-plugin store, the checkout route fires 1,200–2,000 hooks before a single byte of HTML is emitted.
Asset cost. The CSS and JavaScript that render the checkout form in the browser. This is what most guides optimise: defer this script, inline that style, remove this unused stylesheet. It's real. It's also the smallest of the three costs. Removing 180ms of asset load from a page that takes 2,600ms to generate server-side is not a meaningful fix.
Work in this order: boot first, run second, assets third. The reverse order is how you spend two hours on optimisations that move the needle 4%.
The popular fixes, ranked by how much they help
Full-page cache plugins (WP Rocket, LiteSpeed Cache, NitroPack)
As established: zero impact on checkout TTFB. These plugins cache rendered HTML. Checkout HTML isn't cached. Install them anyway — they help every other page on the site. But don't expect them to touch the checkout slowness. They won't.
Disabling cart fragments on checkout
WooCommerce loads wc-cart-fragments on every front-end page. On page load, the script fires a POST to /?wc-ajax=get_refreshed_fragments — a full WordPress boot, uncached, just to return a JSON blob with the current cart item count and mini-cart HTML.
On the checkout page, the cart state is already rendered in the page. The fragment call is redundant overhead. Disabling it there is correct:
add_action('wp_enqueue_scripts', function () {
if (is_checkout()) {
wp_dequeue_script('wc-cart-fragments');
}
}, 99);Impact: real but moderate. You save the round-trip cost of one uncached AJAX request, typically 200–500ms on a cold server. Worth doing. Not a transformation.
Object caching (Redis Object Cache, Object Cache Pro)
Adding a Redis or Memcached layer helps with run cost. Repeated database queries — session reads, cart meta fetches, product data lookups — are served from memory on the second hit within the same request or across requests on the same server process.
On sites on shared hosting without persistent object cache, this typically takes checkout from 3.2s to 2.1s. Meaningful. Object Cache Pro gets closer to the ceiling than the free Redis plugin, especially on high-concurrency stores.
The limit: object caching speeds up the queries that do run, but does not reduce which queries run. If 30 of your 40 plugins fire database queries on checkout for no reason, object caching still pays all 30 queries — just faster. Boot cost is unchanged.
Shipping rate transient duration
WooCommerce caches computed shipping rates in transients with a default TTL of one hour. When the transient expires under traffic load, every checkout request until one regenerates it fires live to the carrier API — USPS, FedEx, ShipStation, EasyPost. On sale days with concurrent traffic this creates a slowdown spike that looks like random latency.
Extending the TTL reduces how often the carrier API is hit:
add_filter('woocommerce_shipping_transient_time', function () {
return 4 * HOUR_IN_SECONDS;
});Apply this if your carrier rates don't change more than once or twice a day and you've confirmed that longer cache windows don't create visible pricing drift. If you use real-time rate calculations for negotiated rates that update frequently, test carefully before shipping this.
What actually moves checkout TTFB
Part 1 — Stop irrelevant plugins from booting on checkout
This is the highest-leverage change available. A WooCommerce store with 35–45 plugins has at most 10–12 with legitimate work to do on checkout. The others run because nothing stops them.
Plugins with no reason to load on checkout:
- SEO plugins (Yoast, Rank Math, SEOPress, All in One SEO)
- Page builder frontend scripts (Elementor, Divi, Beaver Builder, Bricks)
- Social sharing, related posts, reviews aggregator plugins
- Newsletter opt-in, popup, live chat, announcement bar plugins
- Portfolio, booking calendar, events calendar plugins (unless integrated into checkout)
- Image gallery, slider, video embed plugins
Plugins that must load on checkout:
- WooCommerce itself
- Every active payment gateway (Stripe, PayPal, Square — whichever ones are enabled in WC settings)
- Tax calculation plugins (TaxJar, Avalara, WooCommerce Tax)
- Active shipping plugins (WooCommerce Shipping, ShipStation, EasyPost)
- WooCommerce Subscriptions and Memberships if sold on the store
- WooCommerce Blocks if you use the block-based checkout template
- Any plugin that renders into the checkout form itself (custom fields, checkout addons)
The skip logic belongs in a mu-plugin so it runs before WordPress loads the active plugins list. Here's a minimum working version:
<?php
// /wp-content/mu-plugins/checkout-plugin-skip.php
add_filter('option_active_plugins', function (array $plugins): array {
if (is_admin() || wp_doing_ajax() || wp_is_json_request()) {
return $plugins;
}
$uri = strtok($_SERVER['REQUEST_URI'] ?? '', '?');
// Only apply on the checkout URL — adjust if your checkout lives elsewhere
if ($uri !== '/checkout/' && $uri !== '/checkout') {
return $plugins;
}
$safe_to_skip = [
'wordpress-seo/wp-seo.php',
'rank-math/rank-math.php',
'elementor/elementor.php',
'mailchimp-for-woocommerce/mailchimp-woocommerce.php',
'contact-form-7/wp-contact-form-7.php',
'revslider/revslider.php',
// add yours here
];
return array_values(array_filter(
$plugins,
fn(string $p) => !in_array($p, $safe_to_skip, true)
));
});This is the minimal version. It does not handle: dependency checking (if a plugin in your skip list is required by a plugin you're keeping, WooCommerce may fatal), translated checkout slugs (/de/kasse, /tr/odeme), or the wc-api/ payment return URLs that need to be treated like checkout. Read the full post on per-page plugin loading before shipping this — it covers all three gaps and the exact order of operations that makes the pattern safe.
Part 2 — Hook pruning on checkout
Even plugins that legitimately belong on checkout often register hooks for functionality that never fires there. Jetpack, if you use it, registers hooks for sharing buttons, related posts, Photon image CDN, and a dozen other features that produce no output on the checkout page. All of those registrations are execution time.
The right approach is selective removal — keep the plugin loaded, remove the hooks it doesn't need:
add_action('wp', function () {
if (!is_checkout()) {
return;
}
// Remove Jetpack output hooks from checkout
remove_action('wp_head', 'jetpack_og_tags');
// If Yoast must stay loaded (e.g. dependency of another plugin),
// suppress its checkout output rather than letting it run normally
if (class_exists('WPSEO_Frontend')) {
$wpseo = WPSEO_Frontend::get_instance();
remove_action('wp_head', [$wpseo, 'canonical'], 20);
remove_action('wp_head', [$wpseo, 'og_tag_values'], 30);
}
}, 20);This is plugin-specific work — you'll need to audit which hooks fire on checkout for your stack and decide which ones have no output to produce. The pattern: load the plugin if skipping it entirely is risky, but silence the hooks that generate output you don't need. Combine with Part 1 for the plugins you can safely skip altogether.
Part 3 — Payment gateway initialisation order
If your store has Stripe, PayPal, Klarna, and a manual bank transfer option — all four gateways initialise on every checkout request. Each loads its class, reads its settings from the database, checks credentials, and registers its payment form hooks. Stripe's PHP SDK is not lightweight. Four gateway initialisations on every checkout load costs 80–150ms before any gateway-specific logic runs.
On stores with clear regional segmentation, you can reduce the active gateway set per visitor:
add_filter('woocommerce_payment_gateways', function (array $gateways): array {
if (!is_checkout() || is_admin() || defined('REST_REQUEST')) {
return $gateways;
}
$country = WC()->customer?->get_billing_country() ?? '';
// Only initialise Klarna for supported European markets
if (!in_array($country, ['DE', 'AT', 'SE', 'NO', 'FI', 'DK', 'NL'], true)) {
unset($gateways['klarna_payments']);
}
return $gateways;
}, 20);This is store-specific. Don't apply blanket gateway removal without knowing your customer geography. And test with the actual gateways — not just the checkout page rendering, but completed test orders through each payment method.
Five footguns that will wreck checkout
In the order I have personally stepped on them:
1. PHP session locking on concurrent requests
WooCommerce uses WC_Session_Handler to persist cart data. By default this writes to the database, but on some configurations — particularly older hosts or installs that overrode the session handler via a plugin — it falls back to PHP's native $_SESSION.
When it does, and a cart fragments AJAX call fires concurrently with the checkout page load (as it does on every WooCommerce page that has the fragment script enqueued), PHP's session file lock serialises the two requests. One waits. You see 300–600ms of unexplained latency that varies run-to-run and looks like database slowness. It isn't. The database is idle; PHP is holding a file lock.
Diagnose it by temporarily setting define('WC_SESSION_CACHE_GROUP', 'wc_session_id') and watching whether the latency disappears. Fix it by ensuring WooCommerce uses WC_Session_Handler (the default), not a PHP session override from another plugin.
2. Payment gateway return URLs (3DS callbacks)
Several payment gateways — Stripe's 3D Secure flow is the most common case — redirect the customer away for authentication and then return them to a URL like /wc-api/WC_Gateway_Stripe or /?wc-api=wc_stripe. This URL is not /checkout/, so if your plugin-skip logic from Part 1 is a simple URI match, WooCommerce loads but Stripe doesn't — and the 3DS payment confirmation 500s.
I learned this one painfully. Deployed a checkout-skip rule on a staging store, tested with a Stripe test card (4242 4242..., no 3DS), it worked. Took it to production. A customer in Germany hit the 3DS flow on their Volksbank card, the payment return failed, they received no confirmation, they emailed support. The store processed the payment server-side but never landed them on the thank-you page. Untangling that one took two hours.
Fix: add /wc-api to your checkout URI allowlist alongside /checkout. Test explicitly with a 3DS test card (Stripe publishes a list), not just the standard success test card.
3. WooCommerce Subscriptions renewal processing
WC_Subscriptions_Manager processes renewal orders on a scheduled cron using Action Scheduler. The cron request URL is /wp-cron.php?doing_wp_cron=..., which doesn't contain /checkout/ or /wc-api. If your plugin-skip rule fires on cron requests and removes Subscriptions or its dependencies from the active plugins list, renewal orders stop generating, renewal emails stop sending, and the failure is completely silent.
The gate you need: check defined('DOING_CRON') && DOING_CRON and bail from the skip entirely. Action Scheduler also runs via REST (/wp-json/action-scheduler/v1/), so wp_is_json_request() is a second gate. If you sell subscriptions and miss either of these, you'll find out two weeks later when a subscriber emails asking why their renewal didn't process.
4. Block checkout vs. classic checkout
WooCommerce's block-based checkout (available since WooCommerce 7.6, the default in new installs since 8.3) uses the WC Store API — POST /wp-json/wc/store/v1/checkout — to process the order. The classic checkout uses /?wc-ajax=checkout. If your store switched to the Gutenberg checkout block and your optimisation logic is built around wc-ajax intercepts, the order processing path is different and your optimisations may not apply to it — or may break it entirely.
Verify which template you're using: WooCommerce → Settings → Advanced → Page setup → Checkout page. If the page content is the [woocommerce_checkout] shortcode, you're on classic checkout. If it's the Checkout block (in the block editor, it appears as a Checkout block, not a shortcode), your AJAX interception needs to target REST instead.
5. Nonce mismatch between page render and AJAX handler
WordPress nonces are context-dependent: a nonce generated during a full page load (with 38 plugins active) and a nonce validated during an AJAX request (with 15 plugins active, because your skip logic applies to AJAX too) may not match if any of the skipped plugins modify the nonce algorithm via nonce_user_logged_out, auth_cookie_expiration, or related filters.
The checkout form nonce (_wpnonce) is generated at page render time and submitted with the order. If the AJAX handler that processes it runs in a reduced plugin environment, and one of the missing plugins was involved in nonce generation, wp_verify_nonce() returns false and the order won't process. The customer sees "Your session has expired" on a session that hasn't expired.
Fix: ensure your skip logic is consistent across the checkout page render and the AJAX/REST endpoint that processes the order. They should run with the same plugin set, or the AJAX endpoint should run with a superset of what the page rendered with.
A working measurement
A beta store AcceleratorWP runs with: WooCommerce + Subscriptions, Stripe + PayPal + Klarna, 43 active plugins, Astra Pro theme, Elementor used for marketing pages but a classic PHP template on checkout, real EU traffic. Pre-optimisation checkout numbers (median, cold hit, measured at origin with no CDN):
- Checkout page TTFB: 1,920ms
wc-ajax=get_refreshed_fragments: 580ms (fired on checkout page load)wc-ajax=update_order_review: 940ms (fired when shipping method is selected)
After applying the three-part approach — 28 plugins skipped on checkout, hook pruning for Jetpack and WooCommerce subsystems not relevant to checkout, cart fragments removed from checkout, Klarna gated behind EU country detection:
- Checkout page TTFB: 490ms
wc-ajax=get_refreshed_fragments: removed from checkout pagewc-ajax=update_order_review: 280ms
Same hosting. Same theme. Same 43 plugins still active on the pages that need them.
The checkout page went from 1,920ms to 490ms because 28 of those 43 plugins no longer boot on that URL. The order review AJAX dropped because it shares the same optimised boot path as the checkout page. The total improvement was not a result of any single trick — it came from hitting all three cost layers in order.
These aren't world-record numbers. The store isn't on bare-metal dedicated infrastructure. They're "checkout felt slow and now it feels instant" numbers — the kind where the difference shows up in conversion rate without anyone having to instrument it.
The honest question: build it or use a tool?
The mu-plugin approach above is real and it ships. It also needs ongoing maintenance: every new plugin install changes what belongs on the skip list, every WooCommerce major version can rename hooks you're pruning, every gateway addition potentially needs an explicit URI allowlist entry for its return URL.
A reasonable guide:
-
One store, under 30 plugins, you have a staging environment and a test week. Write the mu-plugin yourself. The per-page plugin loading post covers the dependency edge cases this post glossed over. Use a 3DS test card and a subscription renewal test before you go live. Budget two full days including shadow testing.
-
One store with subscriptions, multiple gateways, or a multilingual setup. The footgun surface is larger than it looks. If you want to skip the week of edge-case debugging, use a tool. The cost of a checkout breaking mid-order during a sale period is not a cost worth optimising against.
-
Multiple stores. Don't write one mu-plugin per store and maintain them independently. Either build a shared internal abstraction you version properly, or use something that ships maintained rules. The N-plugin-drift problem hits around store three and doesn't get better.
Where AcceleratorWP fits
AcceleratorWP runs at the mu-plugin layer and applies the boot-cost fix across the entire site — checkout included. For checkout specifically, the Plugin Skipper ships with a pre-built profile for WooCommerce checkout dependency graph: which plugins are safe to skip, which must stay (payment gateways, tax, shipping, subscriptions), and which need per-request-class handling (page builders, headless front-ends, cron workers).
The 300-entry rule library covers the most common WordPress plugins and their checkout-safety classification. When the setup wizard runs on a new site, it matches your active plugin list against that library, proposes skip rules for the ones it recognises, and flags the ones it can't classify for manual review. Shadow mode then runs those rules silently against real traffic for 24 hours before activation — the cron job, the 3DS callback, the translated checkout URL — all surface before the rules are live.
The numbers above (1,920ms to 490ms) are from a site using AcceleratorWP's Plugin Skipper on the checkout URL. The wizard proposed the initial rule set. A developer spent about 30 minutes reviewing the flagged plugins, added the Klarna EU gate manually, and let shadow mode run for 48 hours before activating.
Read about it if you want — or if you'd rather stay in the technique, the two adjacent posts worth reading first are how to disable WooCommerce on non-shop pages (the same boot-cost approach applied to the rest of the site, with the multilingual and block-theme edge cases covered in detail) and the best WooCommerce performance plugins ranking for how the cache-layer tools complement this structural approach.
If you tried any of this and something broke in an interesting way — especially a footgun I didn't list — I'd genuinely like to hear about it. Send notes to my inbox.

