How WooCommerce.com speeds up requests by loading fewer plugins

How we cut WordPress bootstrap cost by loading fewer plugins on specific WooCommerce.com requests


WordPress has a simple plugin model. A site has a set of active plugins, and WordPress loads all of them on every request. For most sites, that is the right tradeoff. Behavior stays predictable, plugins compose freely, and nobody has to reason about which plugin a particular request actually needs.

On a site the size of WooCommerce.com, that simplicity isn’t free.

WooCommerce.com is a large WordPress and WooCommerce application: marketplace pages, account flows, APIs, checkout, partner workflows, search integrations, tracking, and a long tail of operational code.

Most of those plugins are essential somewhere. Only a few are essential everywhere.

That gap is widest on high-traffic pages and narrow API endpoints. A product discovery page doesn’t need marketplace submission tooling. A cached helper endpoint doesn’t need the same plugin graph as checkout. A public docs page doesn’t need order-numbering logic.

The optimization we use is straightforward: for selected routes, we load only the plugins that route needs.

A blog post on WooCommerce.com, for example, usually doesn’t need the payment gateways or tax calculation plugins. On an uncached request those unrelated plugins can account for 10–20% of the time spent generating the page. So there’s a clear case for not loading them at all.

It relies on WordPress option filtering, an early mu-plugin, and route-specific allowlists. The hard part isn’t writing the hook, it’s staying confident the route still works when most of the site isn’t loaded.

This post covers the pattern and the safety rails we added around it.

Plugin bootstrap adds up on large WooCommerce applications

On a typical WordPress request, active plugins load early in bootstrap. Each one can register hooks, instantiate services, read options, load translations, define custom post types, attach REST routes, queue assets for later in the request, or run compatibility logic.

Any one of those costs is fine on its own. Stacked together on a mature WooCommerce application, they add up.

The thing worth noticing is that the active plugin list is global, but request needs are local. A request to /wp-json/wccom-extensions/1.0/search and a request to /checkout/ start from the same active plugin set, even though the code paths that matter are nothing alike.

Full-page and edge caching reduce how often this bites for anonymous page views, but they don’t remove it. Cache misses still happen. API endpoints can be dynamic. Logged-in requests bypass cache. Operational endpoints often need low latency exactly when traffic spikes.

For a large WordPress application, cutting bootstrap work is a real performance lever.

Existing tools solve a different version of this problem

Selective plugin loading isn’t new. Public WordPress plugins already let a site owner disable plugins per page, post type, URL, or context, including Freesoul Deactivate Plugins and Plugin Load Filter.

These are useful, especially when you want a UI for deciding which plugins run where. Some of them ship mu-plugin components too, because selective loading has to happen before normal plugins load.

Our needs were different. We didn’t want a general-purpose UI. We wanted route-owned, code-reviewed rules that live alongside the rest of our platform code, run in CI, and ship through the same controls as everything else.

In our implementation, rules are PHP classes, registration happens in a mu-plugin, and changes go through the same review and production monitoring as any other performance-sensitive code.

Intercepting the plugin list at bootstrap

WordPress keeps the list of active plugins in the active_plugins option. During bootstrap it reads that option and loads each plugin file in the list.

Because options are filterable, an early mu-plugin can rewrite that list before WordPress loads anything:

add_filter(
	'option_active_plugins',
	function ( array $plugins ): array {
		if ( ! should_limit_plugins_for_this_request() ) {
			return $plugins;
		}
		return array_values(
			array_diff(
				$plugins,
				plugins_to_skip_for_this_request()
			)
		);
	}
);

That filter is the core of the pattern. Everything else exists to answer three questions:

  1. Which requests should be limited?
  2. Which plugins are safe to skip for each one?
  3. What stops a limited request from corrupting global state?

On WooCommerce.com a mu-plugin registers route rules early. We match the request URI against exact paths, prefixes, or regular expressions. When a rule matches, it decides which plugins drop out of the active list.

For our internal loader we have a second filter that limits which WooCommerce.com application modules load. That part is specific to our architecture, but the underlying WordPress mechanism is the same.

Route rules decide which plugins can be skipped

Selective loading is expressed as explicit per-route rules, and we keep those rules in code rather than in a settings screen. Each rule covers a route, or a group of routes, and lists the plugins to leave out for that request.

There are two ways you could decide what to leave out.

An allowlist starts from nothing and adds back only the plugins the route needs. That sounds like the clean approach, but on a large site it’s brittle, because implicit dependencies are everywhere. A theme function leans on a class defined by some plugin. A REST handler calls a helper that eventually reaches into another plugin. A cache miss runs code a cache hit never touched. Miss one of those and the route breaks in a way that’s easy to overlook.

An exclusion list works the other way around: start from the site’s normal active plugins and remove the ones you know the route doesn’t need. It’s less theoretically minimal, but far safer. The route keeps everything that might be needed to render the page and only sheds the large, obvious groups such as admin-only tooling, payment gateways, email processing, anything clearly unrelated to the request. That’s the side we lean on most.

Keeping the rules in code rather than a UI helps in review and operations:

  • Each rule reads as a diff in review.
  • A comment can explain why a plugin is kept or dropped.
  • Tests can assert the URL matching and the exclusions that matter.
  • A production incident traces back to a specific rule change.

It also enforces a habit I like: every plugin left in a trimmed route has to earn its place.

Requests too risky to trim

Not every request is a good candidate.

We skip exclusion for broad WooCommerce request types with many dynamic paths and side effects:

  • wc-ajax
  • wc-api
  • rest_route query-parameter requests
  • download requests

That doesn’t mean REST endpoints are off-limits — some pretty-permalink REST routes are targeted explicitly by rules. The line is about predictability. Broad query-parameter entry points can branch into all sorts of behaviour, so they’re poor defaults for exclusion.

Checkout, cart, account, admin, cron, webhooks, payment callbacks, and downloads get extra caution. These touch money, customer data, authentication, entitlement checks, emails, or third-party integrations. The performance upside has to be weighed against the blast radius.

Dependency discovery is the hard part

Working out what a route actually needs is the hard part.

On a dynamic WordPress application there’s no clean static answer. In practice, we read the route code, follow the obvious dependencies, exercise the common request flows by hand, add focused tests for URL matching and endpoint behaviour, roll out incrementally, and watch error logs and performance data after each rollout.

The biggest wins came from focused, high-traffic routes

The goal isn’t the shortest possible plugin list. It’s a faster request that stays reliable.

We started with the highest-traffic surfaces, since trimming bootstrap work off a single request only pays off when that request runs a lot. From there the obvious targets were the endpoints almost every WooCommerce installation calls back to WooCommerce.com, along with high-traffic pages like product pages.

Loading only the required plugins cut memory use on those requests by more than 50%, and brought latency down with it. The call that tells a connected store which paid subscriptions it owns dropped from around 800 ms to 475 ms. The one that reports which of a store’s installed plugins have updates available dropped from 880 ms to 550 ms. The endpoint that runs every time a store is shown the WooCommerce onboarding flow improved by a similar margin.

The same approach extends to customer-facing pages. Most plugins aren’t needed to render a blog page or a product page, which makes them good candidates. An early pass at product pages, removing payment gateways and a few unrelated extensions the page never touches, gave roughly a 10% improvement in page generation time.

Across all of these, the signals worth watching are response time, PHP memory use, database queries, expensive plugin bootstraps, error rates after deploy, unexpected rewrite-rule changes, and route-specific fatals on cache misses.

The biggest wins show up on focused endpoints and high-traffic anonymous pages with large plugin graphs. The weakest candidates are broad, stateful flows where almost anything could turn out to be relevant.

This pattern fits large, engineering-owned WordPress applications

Selective plugin loading is worth a look when:

  • the site runs many active plugins
  • traffic is high enough that bootstrap cost matters
  • specific routes have narrow responsibilities
  • engineering owns deployment and monitoring
  • the team can write and maintain tests
  • the wins can be measured
  • a bad change can be rolled back quickly

It’s usually a poor fit when:

  • the site is small
  • the active plugin graph is already lean
  • routes are highly dynamic and stateful
  • there’s no staging or production monitoring
  • non-technical users are expected to maintain dependency rules
  • the team can’t live with occasional dependency-discovery work

For most WordPress sites this isn’t a first optimization. Caching, query performance, asset loading, object cache behaviour, and the obvious plugin bloat should come first.

The hook is simple; keeping routes safe is the hard part

The main thing we learned is that selective plugin loading is a sharp tool, useful, and easy to cut yourself on.

The WordPress hook makes changing the active plugin list for a request trivial. That ease is a little misleading. A missing plugin might only fail on one product, one locale, one request parameter, one cache miss, or one logged-in state. A bad rewrite flush can break routes that have nothing to do with the request that caused it.

So the implementation should stay deliberately conservative. What’s worked best for us:

  • Prefer excluding the obviously irrelevant over chasing a perfect minimum.
  • Keep route rules close to the code.
  • Comment dependencies wherever the reason isn’t obvious.
  • Test matching behaviour and the critical endpoint responses.
  • Test the route cold — a cache hit skips the code you’re trimming, so it hides broken rules.
  • Cover logged-in and logged-out — a plugin that’s dead weight on an anonymous page can be load-bearing for the logged-in version of the same URL.
  • Make each rule revertible on its own, and treat production monitoring as part of the rollout, not an afterthought. Watch latency, memory, and fatals after every rollout.

Used like this, selective plugin loading lets a large WordPress application keep the flexibility of plugins while skipping work on requests that never needed the whole application. It’s a pragmatic optimization for the architecture most large WordPress applications actually have.


17 responses to “How WooCommerce.com speeds up requests by loading fewer plugins”

  1. I’ve been doing this since around 2015. I’m quite surprised it has taken so long to reach WooCommerce.com. It makes a massive difference to server resources and TTFB.

    1. Thilina Pituwala Avatar
      Thilina Pituwala

      Hi Jake, you’re right. Even though we published this now, the first implementation was done about two years ago. But before that there wasn’t a need for us to make such an optimisation. It’s a tricky decision to make, weighing performance improvements against maintenance overhead. So any large site adopting this approach should consider it carefully.

  2. Nice work.

    Is this going to be made into a proper API so that plugin developers can declare which routes they actually need to be loaded on?

    Would also make sense to upstream this to WordPress itself, seems like a massive and obvious performance win for all sites.

    1. Thilina Pituwala Avatar
      Thilina Pituwala

      Hi Ian, there are a few existing plugins in the WordPress plugin directory that do the same thing. That said, I agree it could be useful to have this as part of WordPress core. It’s something worth evaluating carefully before jumping in, though.

  3. Any code examples to share?

    1. Thilina Pituwala Avatar
      Thilina Pituwala

      Hi Robin, the basic implementation and the concept are quite simple. In the article, under the section “Intercepting the plugin list at bootstrap”, I’ve shared the code to disable selected plugins.

      Based on that you can maintain a list of plugins to exclude for each route. Let’s say you have a high traffic API endpoint which needs only WordPress core and two plugins. Define the plugins in a map like this:

      ‘/wp-json/your-api-endpoint’ => [ ‘plugin_1’, ‘plugin_2’ ]

      And then use $_SERVER[‘REQUEST_URI’] to identify whether the current request is for the API endpoint in question, and disable all the plugins except what’s in the array.

      This has to be done at the mu-plugins level.

      This is the core concept behind the existing tools that use this approach. You can build tooling around this implementation to improve how you match routes, control the matching logic, decide whether to exclude or include the defined set of plugins, identify plugins that can be excluded from a specific route, and so on.

      1. Cheers Thilina, initally I missed the code example. Will try make sense of it and your response to try dray some inspiration for a solution.

  4. I attempted to contribute to this discussion earlier today. My comment was deleted.

    For the record, I shared that we have been implementing selective plugin loading
    in production at rooster.systems, achieving add-to-cart times of ~40ms and
    checkout under 200ms. This work is patent-pending and rests upon analysis of
    over 870 million lines of WordPress plugin and theme code.

    Reference: https://rooster.systems/knowledge-base/

    I find the deletion puzzling, particularly given that on this same day I
    submitted contributions to WooCommerce core addressing product variation
    performance issues — the same issue I originally raised three years ago
    (woocommerce/woocommerce#37629).

    See today’s contributions:
    https://github.com/woocommerce/woocommerce/pull/66882
    https://github.com/woocommerce/woocommerce/issues/66890
    https://github.com/woocommerce/woocommerce/issues/66892

    I have been a contributor to this ecosystem for decades. Deleting a relevant,
    on-topic comment from someone actively contributing to your codebase — on the
    same day — is a strange way to foster community.

    If my comment violated a guideline, I’d appreciate knowing which one.

    Dimitris Vayenas
    Oxford Metadata

    1. Brent MacKinnon Avatar
      Brent MacKinnon

      Hey Dimitris, your original comment was flagged as self-promotion, as you’re using our comments for promoting and linking out to your own solution.

      1. Hi Brent,

        Thank you for the clarification.

        I respectfully disagree that citing prior art constitutes self-promotion.
        The post presents selective plugin loading as a novel internal development,
        with no acknowledgment that others have been working in this space — some
        with patent-pending approaches and production deployments that exceed the
        performance numbers cited here.

        Jake’s comment — “I’ve been doing this since 2015” — was approved. That’s
        also a claim of prior work. The difference is that mine included a link
        to verifiable documentation and specific results.

        For context: our work in this area has been communicated publicly and
        directly to members of the WooCommerce team. The challenges outlined
        in your post as “hard problems” — dependency discovery, cross-request
        intelligence, safety at scale — have working solutions in our implementation.

        I understand if WooCommerce prefers to reinvent the wheel rather than
        engage with existing work in this space. But characterizing a relevant
        technical contribution from a same-day core contributor as “self-promotion” —
        while publishing a post that omits any mention of prior art — is noted.

        I’ll continue contributing where I can.

        Dimitris Vayenas
        Oxford Metadata

        1. Thilina Pituwala Avatar
          Thilina Pituwala

          Hi Dimitris,

          On prior art — the post cites both Freesoul Deactivate Plugins and Plugin Load Filter as established solutions in the directory. It doesn’t claim the concept is novel; it documents how we applied it at WooCommerce.com’s scale.

          On reinventing the wheel — we deliberately chose the narrow, static approach over a dynamic one. A few high-traffic endpoints didn’t need most of the plugins the rest of the site loads, but weren’t independent enough to decouple entirely. We could have built a component to analyse code paths and feed exclusions back dynamically, but the maintenance burden and the risk at our traffic volumes outweighed the benefit. That’s a fit decision for our infrastructure, not a judgment on other implementations.

          It’s been running for over two years without major issues, which is the result the post reports.

  5. Robert Baumann Avatar
    Robert Baumann

    Interesting and pragmatic approach. One architectural limitation may be worth making more explicit, though.

    The mu-plugin can filter active_plugins before regular plugins are loaded, but the decision still happens inside the WordPress bootstrap. At that point, the resolved WordPress and WooCommerce context does not yet exist. The loader therefore has to infer request intent from raw signals such as REQUEST_URI, prefixes, regex patterns, query parameters, and cookies.

    That explains several characteristics described in the article:

    route rules must duplicate parts of the application’s routing knowledge
    dependency discovery remains largely manual
    exclusion lists are safer than true minimal plugin sets
    broad or stateful requests have to remain mostly untouched
    correctness depends heavily on testing, monitoring, and continued maintenance

    This makes the pattern highly useful for narrow, predictable, high-traffic routes. But it is not yet a general context-aware solution for preventing unnecessary plugin execution across a dynamic WordPress application.

    The enforcement point is early enough to prevent regular plugin includes. The unresolved part is how to determine the required execution scope reliably before WordPress itself has established the request context.

    1. I truly want to reply to your message with some tips but I am going to be told off that I am showcasing our approach 🙂

      A quick answer though is no, you can have your optimization rules even outside from the mu-plugins path. Before even WP takes charge. This approach shave many ms.

      To implement this however you need to do some work in nginx, you have to have access to internals that usually most hosts (like kinsta, Cloudways etc) do not provide. This is why we ended up even creating our own panel.

      I hope this helps.

      1. Robert Baumann Avatar
        Robert Baumann

        You should read this dev.to post to understand my intention about this topic.
        https://dev.to/rushdev/beyond-woocommercecoms-selective-plugin-loading-59hh

        1. Thank you!

          I responded already! 😉

          Feel free to sign up to our optimizer and/or the panel if you would like to experience the additional benefits of not serving the compiled optimizers via mu-plugin.

          Here are some indicative results about our optimization.

          Request No optimisation mu-plugin Server-level Server-level + L1 Boost
          update_order_review (checkout AJAX) ~800 ms 142 ms ~97 ms 78.8 ms
          /checkout (full page) ~2,800 ms 516 ms ~264 ms 131.54 ms

    2. Thilina Pituwala Avatar
      Thilina Pituwala

      Hi Robert, agree with this, and I’ve gone through the ideas you shared in the linked post.

      WooCommerce.com is hosted on WPVIP with solid auto-scaling infrastructure. But we noticed that in some cases, traffic to the APIs used by WooCommerce core was dragging down the performance of the whole application, due to the sheer volume of requests and the traffic spikes. That’s where plugin exclusions came in. We had a few high-traffic endpoints that didn’t use the majority of the plugins the rest of the site relies on, but they weren’t independent enough to be decoupled from WooCommerce.com entirely.

      That’s why the narrow, static, and more testable approach worked for us, and from my understanding, it’s the better fit for a large site like WooCommerce.com. We could have built a dynamically evolving exclusion list by adding a component to analyse actual code paths and feed that back into the exclusion rules. But we decided to stick with the simplest approach, since the maintenance burden on the team and the risks involved outweighed the benefits.

      I think both solutions you and Dimitris described here will be very useful for mid-sized WordPress sites. But for a site like WooCommerce.com, the dynamic approach feels too risky to me.

      WooCommerce.com has been running with selective plugin loading for more than two years without any major issues, so I believe that if it’s done right, this approach works reliably for high-traffic sites.

      1. Thilina hi,

        Thanks for the clarification. Please allow me to put my hat as some one who has been involved with static code analysis for many decades. My expertise is in writing meta-compilers that can reverse engineer any language and provide us with what the code can potentially do not just what it does. Static code analysis is an old technique initially used for software quality assurance for NASA and Nuclear Power Station control systems. Slowly it is been democratized. Now our clients include companies primarily in the Financial Sector. The firm I am collaborating in the IBM mainframe ecosystem has clients that have a combined AUM of over $50 Trillion. But it is one of these boutique consultants that one rarely hears about.
        At Oxford my supervisor Prof. Jeremy Gibbons was involved with “code that writes code” decades before the broader public heard the term LLM.
        I am not writing the previous to flaunt who I am, I am just trying to re-assure you that what may sound “risky” is been tried and tested, robust enough techniques, since ages.
        I have to admit that it has been painful to observe that my initiative to bring to the WordPress ecosystem this knowhow (for free as you know) not only has received any support but was subject to moderation in this very thread.
        After 45 years of involvement with Computer science – my first computer was back in 1981 (aged 12) and my first computer programs were published in magazines aged 14, this moderation offended me. I hope that the above background will help you understand why this is so.

        A final point. It is sad to see (or sense) that one is treated as an adversary rather than a partner, just because he/she informs that this issue has been solved. It is only a matter of support from you (because the platform is yours) to make use of it, given the urgency of the situation. Unless of course you sense that having Woo performing x10 faster with 3x less resources is something that can… wait.

        Thanking you for your consideration.

Leave a Reply

Your email address will not be published. Required fields are marked *