The first time you measure a WooCommerce site and look at what runs on the homepage, the number is uncomfortable. WooCommerce itself, plus a half-dozen extensions you forgot you installed — Subscriptions, Stripe, Klaviyo, a wishlist plugin, a currency switcher, the rebate engine, six different shipping calculators that all initialise on boot — every single one of them, on every single request. Your About page pays for the cart. Your blog post pays for the cart. The 404 page pays for the cart. The cart isn't even there.
So you Google "disable WooCommerce on non-shop pages." You get advice. The advice is consistent, surface-plausible, and quietly wrong about half the time. The other half, it works on your laptop and breaks on production three weeks later, the moment a customer hits the checkout from /tr/odeme and the gateway plugin is missing.
This post is the version of the answer I wish I'd found two years ago — what actually works in 2026, what looks like it works but doesn't, and where the line sits between "ship a snippet" and "use a real tool."
What people Google, and what they actually want
The query is one of these:
- "disable woocommerce on non-shop pages"
- "wp_dequeue woocommerce styles"
- "woocommerce slow homepage"
- "remove woocommerce assets from non-shop pages"
- "stop woocommerce loading everywhere"
Underneath all of them is the same observation. The site loads WooCommerce on /about even though /about has no product, no cart, no checkout, no my-account. The page weight is 200 KB heavier than it needs to be, the TTFB is 300–500 ms higher, and the Core Web Vitals tab in Search Console is starting to make polite noises.
What the operator wants: "load the WooCommerce stuff only when WooCommerce stuff is actually shown." It sounds reasonable. It is not reasonable, because of one detail almost no tutorial mentions.
The detail nobody mentions
WooCommerce isn't only the product page, the cart, and the checkout.
WooCommerce is also: the mini-cart widget you put in the header on every page. The shortcode for "featured product" on the homepage. The Klaviyo identify event that fires on every visit. The session cookie set on first request to enable cart persistence later. The geolocation lookup that runs to figure out whether the visitor sees prices in EUR or USD. The wc_clear_notices housekeeping hook on init. The currency switcher reading from WC()->session. The frontend script that injects the cart count badge after page load. A REST handler that hydrates a "recently viewed" widget. The cron sweep that processes failed payments at 4 a.m.
All of those things touch WooCommerce. Many of them touch it on URLs that aren't /shop, /cart, /checkout, or /my-account. If you globally disable WooCommerce on "non-shop pages," at least one of them breaks. Often three or four.
Which is why the popular advice fragments into three flavours, all of which sort of work and none of which sort of doesn't.
The three popular fixes, ranked by how much they break
Fix 1: Dequeue scripts and styles
The most common version of the advice. It looks like this:
add_action('wp_enqueue_scripts', function () {
if (function_exists('is_woocommerce') && !is_woocommerce() &&
!is_cart() && !is_checkout() && !is_account_page()) {
wp_dequeue_style('woocommerce-general');
wp_dequeue_style('woocommerce-layout');
wp_dequeue_style('woocommerce-smallscreen');
wp_dequeue_script('wc-cart-fragments');
wp_dequeue_script('woocommerce');
}
}, 99);This does save assets — about 50–80 KB of CSS and JS off your homepage. What it does not save is anything server-side. Every PHP file in woocommerce/ was already loaded. Every hook is already registered. Every add_filter and add_action from every active plugin is still there. The TTFB is unchanged. The page weight drop is real but small.
It also breaks the cart count badge in your header on the homepage, because wc-cart-fragments is what updates that badge over AJAX. Half the time it stays at the value the page rendered with; the other half it disappears entirely until the visitor navigates to a different page.
So: real but small win, easy regression, no server-side benefit. Most operators find this one and stop here. If your only complaint is page weight, fine. If your complaint is TTFB, this isn't your fix.
Fix 2: option_active_plugins to disable WooCommerce on URL match
Bigger hammer. Same shape as the trick I covered in the previous post:
// /wp-content/mu-plugins/skip-woocommerce.php
add_filter('option_active_plugins', function ($plugins) {
if (!isset($_SERVER['REQUEST_URI'])) return $plugins;
$uri = $_SERVER['REQUEST_URI'];
$shop_paths = ['/shop', '/product', '/cart', '/checkout', '/my-account'];
foreach ($shop_paths as $needle) {
if (strpos($uri, $needle) !== false) return $plugins;
}
return array_diff($plugins, ['woocommerce/woocommerce.php']);
});This actually saves the TTFB. WooCommerce's PHP doesn't load at all on /about. You'll see a 100–300 ms drop in TTFB on a typical 30-plugin store, larger if you have heavy WC extensions like Subscriptions or Bookings layered on top.
It also breaks. Predictably. Catastrophically.
The list of things this snippet doesn't catch:
-
The shop URL isn't
/shop.wc_get_page_id('shop')returns whatever page the operator picked, and on most stores it's the homepage. So your/shopURL match passes nothing through, and the homepage loads without WooCommerce, and the homepage's "Featured Products" block returns a fatal. -
Translated slugs. The Turkish store at
/tr/sepet(/tr/cart) doesn't match/cartand doesn't load WC. The user clicks "checkout," the gateway plugin tries to callWC(), the page returns a 500. -
AJAX requests. Every
admin-ajax.php?action=woocommerce_get_refreshed_fragmentscall, fired by the cart-fragments script on every page, doesn't match any of the shop URLs. WC isn't loaded. The fragment update fails silently. The cart count goes stale. -
REST API.
/wp-json/wc/v3/productsis hit by the headless frontend, the mobile app, the Stripe webhook handler, the Klaviyo sync. None of those URLs contain/shop. WC isn't loaded. The request 404s because the route was never registered. -
Block themes. A homepage built with the Site Editor that contains a
woocommerce/featured-productblock has no/shopin its URL — it's/. Block renders, throws.
The pattern is always the same. The snippet works on the URL the author tested it on. It breaks on every URL the author didn't think of. Some of those URLs are the most important ones on the site.
Fix 3: Detect with WooCommerce's own conditionals
Tighter version, leaning on WC's helpers:
add_action('plugins_loaded', function () {
add_action('wp', function () {
if (is_admin() || wp_doing_ajax() || wp_is_json_request()) return;
if (is_woocommerce() || is_cart() || is_checkout() || is_account_page()) return;
// Strip WC frontend hooks
remove_all_actions('woocommerce_init');
remove_all_actions('woocommerce_before_main_content');
// ... etc
}, 1);
});Surgically removes WC's frontend hooks on pages that aren't WC pages, while leaving WC itself loaded. So the cart fragments AJAX still works, the REST API still works, the cron still works. The frontend doesn't render WC's wrapper markup.
This is closer to right. It also doesn't help your TTFB much, because WC was still loaded — you saved the cost of running its frontend templating, which is real but small. Maybe 30–80 ms on a typical store. Not nothing. Not 300 ms.
It's also fragile in exactly the way custom hook removal always is. WooCommerce ships a new version, the hook names change, your remove_all_actions('woocommerce_before_main_content') is now a no-op, the regression slips into production unnoticed.
The honest mental model
Three different things are happening, and the popular advice keeps mixing them up:
-
Boot cost. WC's PHP files loading into memory, classes registering, hooks attaching. This is the big-ticket item — it's what you pay for "WooCommerce is active."
-
Run cost. WC actually doing work during the request — running queries, fetching the cart, calculating shipping, geolocating the visitor, dispatching emails.
-
Asset cost. WC's CSS and JS being enqueued and shipped to the browser.
Each one wants a different fix. Asset cost is solved by wp_dequeue_*. Run cost is solved by hook removal or selective subsystem skipping. Boot cost is the only one solved by option_active_plugins, and it's the only one that saves real TTFB.
The fix-1 advice goes after asset cost. The fix-2 advice goes after boot cost (and breaks). The fix-3 advice goes after run cost. Most blog posts don't draw the distinction, and most operators don't realise they're solving the wrong layer until they've measured.
What actually works in production
The right move on a serious WooCommerce store has three parts, in order:
Part 1 — Skip WooCommerce entirely on cold pages
Cold pages are the URLs where, if WooCommerce never existed, nothing would change. Examples:
- The
wp-cron.phprequest that fires every few minutes /feed/and other RSS endpoints- A static
/aboutpage with no shortcodes, no blocks, no widgets that touch WC - Most blog posts on most stores
- The robots.txt and sitemap.xml routes
These can have WooCommerce stripped from option_active_plugins cleanly. The pattern is the inverse of what tutorials tell you: don't list the URLs to skip on, list the conditions under which it's safe to skip.
add_filter('option_active_plugins', function ($plugins) {
if (is_admin() || wp_doing_ajax() || wp_is_json_request()) return $plugins;
if (defined('DOING_CRON') && DOING_CRON) return $plugins; // cron uses WC
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
// Bail if we even *might* be on a WC URL
foreach (['/shop', '/product', '/cart', '/checkout', '/my-account', '/wc-api'] as $needle) {
if (strpos($uri, $needle) !== false) return $plugins;
}
// ... and a per-site allowlist of the operator's translated slugs
// and any custom URLs ('/orders', '/wholesale', '/dropship-portal')
foreach (apply_filters('mysite_wc_url_fragments', []) as $needle) {
if (strpos($uri, $needle) !== false) return $plugins;
}
return array_diff($plugins, ['woocommerce/woocommerce.php']);
});Even this is the minimum version. To make it actually safe you need: the dependency check (other plugins that require WC will fatal if you skip it), the block-aware check (a page with woocommerce/featured-product block is a shop page even on /), the language-aware check (/tr/sepet, /de/warenkorb), and a kill switch that puts everything back if anything goes wrong.
Part 2 — Prune WooCommerce subsystems on warm pages
Warm pages are URLs where WooCommerce could be relevant, but specific subsystems aren't. The single biggest example: the homepage of a store that has a "Featured Products" block but no cart, no checkout, no my-account.
WooCommerce on that homepage runs:
- Its session manager, to set the cart cookie
- Its geolocation client, to detect visitor country
- Its currency switcher, reading the session
- Its cart fragments AJAX endpoint registration
- Its analytics integrations (Klaviyo, GA4 enhanced ecommerce)
- Its frontend scripts queue for cart-fragments and add-to-cart
Featured Products only needs the product query. Everything else is overhead. On a real store I measured this at 240 ms of homepage TTFB — the page rendered fast once anyone hit the cart, but the first cold homepage view paid the full WC stack.
The right tool here is selective hook removal — surgically disable woocommerce_init subsystems that the page doesn't need, while leaving the product query path intact:
add_action('wp', function () {
if (is_admin() || wp_doing_ajax() || is_cart() || is_checkout() || is_account_page()) {
return;
}
if (function_exists('WC')) {
// Skip session init on pages that have no cart interaction
remove_action('woocommerce_init', [WC()->session, 'init_session_cookie'], 5);
// Skip geolocation on pages that aren't pricing visitor by location
remove_filter('woocommerce_get_geolocation', [WC_Geolocation::class, 'get_geolocation']);
// ... etc
}
}, 1);This works, but it's the part that bit-rots fastest. WooCommerce reorganises its hook names every couple of major releases. A remove_action call with the wrong priority is a silent no-op. You won't notice the regression until your TTFB creeps back up, six months later, and you can't remember why you wrote this in the first place. Comment heavily, write a self-test that confirms the hook is actually still attached to the priority you think it is, and re-run it on every WC update.
Part 3 — Kill assets last, after boot and run are handled
Now wp_dequeue_* saves what it should save: the bytes that go to the browser. Don't lead with this one. Lead with boot, then run, then assets. In that order, the wins compound.
add_action('wp_enqueue_scripts', function () {
if (is_admin() || is_cart() || is_checkout() || is_account_page()) return;
if (function_exists('is_woocommerce') && is_woocommerce()) return;
if (has_block('woocommerce/featured-product') || has_block('woocommerce/all-products')) return;
foreach (['woocommerce-general', 'woocommerce-layout', 'woocommerce-smallscreen'] as $h) {
wp_dequeue_style($h);
}
foreach (['wc-cart-fragments', 'woocommerce', 'wc-add-to-cart'] as $h) {
wp_dequeue_script($h);
}
}, 99);That has_block check is the part most snippets miss. Block themes mean your "non-shop" homepage might absolutely have a WooCommerce block on it.
Five footguns the snippet fans never mention
In the order I have stepped on them:
1. The cart-fragments AJAX call
This is the silent regression killer. The wc-cart-fragments script fires ?wc-ajax=get_refreshed_fragments on every page load on every URL. If WC isn't loaded on the URL, the AJAX call returns garbage. The cart count badge goes stale. The customer adds a product, the badge still says "0," they assume the click didn't work, they leave.
The fix is to either keep WC loaded on all URLs (Part 2 / Part 3 only, no Part 1), or to also dequeue the cart-fragments script on URLs where you're skipping WC entirely. The second option is correct but requires you to coordinate the boot-time skip with the asset-time dequeue. The order matters: the dequeue runs after WC isn't loaded, so the script handle doesn't exist, so wp_dequeue_script is a no-op. You need to suppress the enqueue too — or dequeue the placeholder using wp_register_script first.
2. Block themes and the Site Editor
A pattern I see all the time in 2026: the operator switched to a block theme last quarter and built the homepage in the Site Editor. The homepage has a woocommerce/featured-product block, a woocommerce/all-products grid, and a woocommerce/cart shortcode in the header. None of these put /shop or /product in the URL. The URL is /. Your URL-based detection fires "skip WC," WC isn't loaded, the homepage now renders three error blocks where products were.
The right detection for block themes uses has_block() — not URL match. URL match is a 2018 mental model. By 2026, "is this a shop page?" is a question about the content of the page, not the URL.
3. Translated slugs
I covered this in the previous post too, but it bites WooCommerce harder than it bites anything else. A multilingual WC store has a translated checkout URL on every language. /checkout, /tr/odeme, /de/zur-kasse, /es/pago, /fr/commande. Your strpos($uri, '/checkout') !== false skips loading WC on every locale that isn't English. Customer hits the localised checkout from a regional campaign, payment gateway plugin tries to call WC(), page 500s, customer goes to a competitor.
The plugins that do this routing — WPML, Polylang, TranslatePress — expose the localised slugs through their own APIs (pll_translate_url(), apply_filters('wpml_object_id', ...)), but most blog snippets don't bother. The right answer is either to enumerate the translated URLs explicitly, or to use a language-aware match mode that handles common locale prefixes natively.
4. The REST API and headless setups
If your store has a headless frontend (Next.js or Astro reading from /wp-json/wc/v3/), or a mobile app, or a Zapier integration that polls orders, every one of those is a request that needs WC loaded but has no /shop in the URL. It's /wp-json/wc/v3/orders, /wp-json/wc/v3/products, /wp-json/wc/store/cart, /wp-json/wc/store/checkout. None of those match a URL-fragment skip rule.
The right gate is wp_is_json_request() — bail out of the skip if it's true. Most snippets miss it. The fatal you get is a 404 on the REST endpoint, which tends to be invisible in browser testing because browsers don't hit /wp-json/. Your headless frontend developer hits it, two days after deploy, and pings you in a slack channel that hasn't been used since 2024.
5. The 4 a.m. cron
WooCommerce schedules cron jobs to clean up expired sessions, retry failed payments, send abandoned-cart emails, and rotate transients. These run via wp-cron.php (or a system cron pointed at it). The request URL is /wp-cron.php?doing_wp_cron=..., which doesn't contain /shop. WC doesn't load. The cron job fails silently. Your "abandoned cart" email campaign stops sending. You don't notice for a week. The week's lost revenue is roughly equivalent to the ROI of the entire optimisation.
The gate for this is defined('DOING_CRON') && DOING_CRON. Most snippets miss it. If you're running Action Scheduler — which most stores are, because WooCommerce ships it — there's a similar gate (ActionScheduler::is_initialized() after WP boot). Either way, cron runs deserve their own skip-bypass condition.
A working measurement
A beta tester runs a multilingual WooCommerce membership platform — Subscriptions, four payment gateways, custom dashboard for instructors, Astra Pro theme, ~40 plugins, real traffic across four locales. Pre-optimisation:
- Homepage TTFB: 720 ms (mobile, regional, cold cache)
- About page TTFB: 680 ms
- Logged-in dashboard AJAX: 1.4 s per request
After applying the three-part approach (skip WC on cold pages, prune subsystems on warm pages, dequeue assets on non-shop URLs), with the cart-fragments coordination and the language-aware rule:
- Homepage TTFB: 280 ms
- About page TTFB: 240 ms
- Dashboard AJAX: 220 ms
Same site, same hosting, same theme. The TTFB drop is from skipping WC's PHP boot on the cold URLs (Part 1), the dashboard AJAX drop is from the admin-ajax isolator cutting the per-request boot cost on a different request class.
These aren't world-record numbers. They're "the site goes from feels-slow to feels-fast" numbers. The kind of numbers that, if you're an agency, the client doesn't have to be told about — they just stop complaining.
The honest scaling problem (again)
Doing this for one store, one operator, one rule set: feasible. Two days of work, one day of shadow soak, ship.
Doing it across a portfolio: not feasible. Every store has a different page-id mapping for /cart and /checkout. Every store has different translated slugs. Every store has different blocks on its homepage, different REST endpoints exposed, different cron schedule, different headless integration patterns. The mu-plugin grows from 50 lines to 300 lines and starts to look like a small framework. You start to need version control on it, you start to need shadow-soak telemetry on it, you start to need a way to roll back rules that misbehaved last night.
I built AcceleratorWP because — same as the previous post said — I got tired of writing the same 300-line mu-plugin across every WooCommerce client. It does the three-part approach above, with the dependency safety net, the block-aware detection, the cart-fragments coordination, the language-aware matching, the REST and cron guards, the shadow soak window, and a rule library that ships with safe defaults for ~40 of the most common WooCommerce extensions out of the box.
The free tier covers the rule library and Plugin Skipper. The premium adds per-page Keep/Skip overrides, Hook Pruner for the warm-page subsystem trim, and the AJAX Action Isolator for the dashboard slowdown. Pricing here.
That's the disclosure paragraph. The technique above works regardless of which tool you use, or whether you write your own.
Decision tree
A reasonable test:
-
One store, no multilingual, no headless, blocks-light. Write the mu-plugin yourself. Apply the three-part pattern. Shadow-test for a week. Total time: two days. The gain is real and you'll learn the WC internals that show up in every other future project.
-
One store, but it's the one that pays your bills. Don't write the half-baked version. Either get the snippet 100% right after a long shadow soak, or use a tool. The cost of getting it wrong on production checkout is one of those Mondays you remember for years.
-
Three to five stores, varying stacks. Write a shared mu-plugin, version it in a private repo, deploy with WP-CLI across stores. Maintain it like a real piece of software — tests, CI, changelog, the works. By store six this becomes a part-time job.
-
Six or more stores, or any store complex enough to hit two of the five footguns. Use a tool. Mine, Plugin Organizer, or hand-rolled with a real internal abstraction — pick one, commit, and maintain that one. The cost of N hand-rolled mu-plugins drifting out of sync exceeds any plausible license fee inside of a year.
What the answer looks like, regardless of the tool
Whatever path you take, the working version of "disable WooCommerce on non-shop pages" has the same shape:
- The boot cost is what saves TTFB. Asset dequeue is real but smaller. Don't confuse the two.
- Skip rules are written by request class first — frontend page, AJAX, REST, cron, admin — each gets its own answer.
- Block-aware detection beats URL matching for any site built in the Site Editor. Use
has_block(), not URL fragments. - Translated slugs need an explicit map or a language-aware rule mode. URL fragment matching is monolingual by default and that default bites multilingual stores.
- Cart-fragments AJAX is the regression-killer — coordinate the boot-time skip with the asset-time dequeue, or you'll have a cart-count badge that updates 50% of the time.
- Every new rule lives in shadow mode for a realistic traffic window before it touches real visitors. Real traffic surfaces the cron job, the headless poller, and the regional checkout that your local browser tab will never reach.
If your version hits all six: ship it. If it doesn't, the bug will live in production for months, and you'll find out about it the day a customer's checkout 500s mid-cart on Black Friday.
If you'd rather skip the year of accumulated bug reports — run the wizard on a staging copy and let it propose the rules. The free tier handles the most common 80%. Whatever's left, you can either configure manually or stay on the free tier and write the remaining 20% yourself, with a much smaller surface area than the full 100%.
Either way: please don't drop the dequeue snippet into functions.php and call it done. The cart count will lie to your customers and you won't find out until it's already cost you a few of them.

