2 sites for $49.99 ·

AcceleratorWP
All posts
Performance

Load a WordPress Plugin on Specific Pages Only — Done Right

Every WordPress tutorial tells you to drop an option_active_plugins filter into functions.php. It silently fails on every site I've ever tried it on. Here's what actually works in 2026.

Sarp EfeMay 6, 202613 min read

The first thing every WordPress developer learns when a site gets slow is this: not every plugin needs to run on every page.

Your SEO plugin doesn't need to do anything during a cron sweep. Your booking widget doesn't have to load on the About page. Your SVG-support plugin definitely isn't earning its salary on the admin-ajax endpoint your custom dashboard hits forty times a minute. And yet WordPress, by default, will loyally boot every active plugin on every single request, regardless of whether it has anything to contribute.

So you Google "how to load a WordPress plugin only on specific pages." You get a wall of tutorials suggesting filter hacks. add_filter('option_active_plugins', ...), drop it in functions.php, you're done. Plug it in, refresh, congratulate yourself.

Except it doesn't work the way you'd expect, and most of those tutorials never tell you why.

I've shipped variations of this code across about a dozen client sites over the last three years. Every single time, something blew up that the tutorial author didn't mention. This post is what I learned the hard way — what actually works, why the popular advice breaks, and what to do once you're trying to maintain this trick across more than one site.

A WordPress admin Plugins page with 47 active plugins, scrolled halfway down
A WordPress admin Plugins page with 47 active plugins, scrolled halfway down

What people Google, and what they actually want

The query is usually one of these:

  • "load wordpress plugin on specific pages"
  • "disable wordpress plugin on certain pages"
  • "conditional plugin loading wordpress"
  • "load plugin only on one page wordpress"

Underneath all of these is the same problem. The site has 30, 40, 50 active plugins. Most of them are needed for something — but not for every request. The user wants a way to say "only load Contact Form 7 on the contact page" or "only run WooCommerce on URLs that are actually shop pages." It sounds reasonable. It should be easy.

It is not easy. The reason is timing.

Here's the recipe people copy from blog posts:

// In your theme's functions.php — DO NOT
add_filter('option_active_plugins', function ($plugins) {
    if (strpos($_SERVER['REQUEST_URI'], '/contact') === false) {
        $plugins = array_diff(
            $plugins,
            ['contact-form-7/wp-contact-form-7.php']
        );
    }
    return $plugins;
});

It looks plausible. It reads cleanly. It works in zero environments.

The problem is where you put it. By the time functions.php runs, WordPress has already loaded every active plugin. The option_active_plugins option is read inside wp-settings.php, way before your theme is even touched. So a filter registered in functions.php fires after the very thing it's trying to influence has already happened. Your CF7 still boots. Your local test still passes because the page renders. You don't notice the regression until you check Query Monitor and see, yes, all 47 plugins still load on /about.

The fix is to register the filter from somewhere that runs before WordPress reads the active plugins list. There is exactly one official place in WordPress that runs that early.

The mu-plugins layer is what you actually want

mu-plugins — "must-use plugins" — live in /wp-content/mu-plugins/. WordPress loads them before normal plugins, and you can't deactivate them through the admin. Anything you put in here runs early enough to influence option_active_plugins.

A working version of the trick looks like this:

<?php
// Save as /wp-content/mu-plugins/conditional-plugin-loader.php
 
add_filter('option_active_plugins', function ($plugins) {
    if (!isset($_SERVER['REQUEST_URI'])) {
        return $plugins;
    }
 
    $uri      = $_SERVER['REQUEST_URI'];
    $on_contact = strpos($uri, '/contact') !== false;
    $on_admin   = is_admin();
    $on_cron    = defined('DOING_CRON') && DOING_CRON;
 
    // Skip CF7 unless we're on /contact or in the admin
    if (!$on_contact && !$on_admin) {
        $plugins = array_diff(
            $plugins,
            ['contact-form-7/wp-contact-form-7.php']
        );
    }
 
    return $plugins;
});

This actually works. It's also where things stop being simple.

Five things this naive version breaks

In rough order of how often I have stepped on them.

1. Dependency chains

WooCommerce is required by twelve other plugins on a typical store. Skip WooCommerce on the homepage to "save load time" and watch the store's product widget on the homepage suddenly throw a fatal error. The dependent plugin is still active, still calls WC(), still expects WooCommerce's classes. They are not there.

What you actually need is a check: does any other active plugin require this one? If yes, refuse the skip. WordPress doesn't track this for you. You have to read every active plugin's Requires Plugins: header — added in WordPress 6.5 — build a dependency graph, and check it on every request before you skip anything.

This is exactly what the dependency safety net does in our plugin, and it's the part that took the longest to get right.

2. The wrong request type

is_admin() lies. It returns true for admin-ajax requests too. So if you condition "skip on frontend, keep on admin," a plugin that only matters for admin pages will still load on admin-ajax — even though admin-ajax calls don't render the admin UI.

You actually need to classify the request more precisely than that:

  • A frontend page view from a logged-out visitor
  • A frontend page view from a logged-in user
  • A REST API read
  • A REST API write
  • An admin page render
  • An admin-ajax dispatch (which is a third category, not "admin")
  • A cron run (wp-cron.php or ?doing_wp_cron=)
  • A feed (/feed/, /feed/comments-rss2/, etc.)

Each one wants a different plugin set. We expose this as a request class in the telemetry — every request is tagged so you can see which class is dominating your site. On most stores it turns out to be admin-ajax, by a lot. Most operators have no idea until they actually look.

3. Multilingual sites

If your rule is "skip Contact Form 7 unless the URL contains /contact," what about /tr/iletisim? /de/kontakt? /zh-tw/聯絡我們? Plugins like WPML, Polylang, and TranslatePress all use slug prefixes by default, but the slugs themselves can be translated — and they often are.

A visitor going to /de/kontakt doesn't trigger the rule. The plugin doesn't load. The form returns a fatal. One of our beta testers — running a multilingual membership platform with translated slugs across several locales — hit this exact problem on the contact and checkout pages. The answer was a language-aware match mode that handles common locale prefixes automatically, plus a manual "extra targets" field for the cases where the slug itself is translated and prefix matching doesn't help.

4. WooCommerce checkout, of course

The hardest URL to skip anything on is the WooCommerce checkout page, which is a normal post under the hood (looked up via page_id setting), routed through WC's templating system, that depends on the cart, which depends on session, which depends on a half-dozen plugins for payment gateways, shipping calculation, geolocation, currency conversion, tax. Disable any of them on /checkout and something breaks.

The right move is usually the inverse: keep your whole plugin stack loaded on checkout, and use per-page Skip rules on every other URL where 90% of those plugins do absolutely nothing. Edge case becomes the rule, default behavior is the exception.

5. Logged-in admin users

Caching plugins universally bypass cache for logged-in users. Your conditional plugin loader, on the other hand, runs on every admin page view and every admin-ajax call from those users. If you have the wrong condition, you hit the fatal as the site editor.

I learned this one painfully. I deployed a "skip Rank Math on the front-end except /about" rule, an editor opened the post editor, Rank Math's metabox tried to render, Rank Math wasn't loaded, white screen, panicked client. Frontend condition ≠ admin condition. Always test in shadow soak — observe the rule for a few hours of real traffic before flipping it live.

The honest scaling problem

Doing this for one site — one developer, one plugin, one rule — is fine. You can write the mu-plugin in twenty minutes, test it for an hour, ship it. It works.

The trouble starts at site number two. Now you need a different mu-plugin, with different rules, for a different stack. Site number five is where the math breaks. You have 200 lines of conditional logic per site, no version control across sites, no telemetry showing whether the rules even fire on real traffic, and a vague memory that you also need to update the dependency graph by hand whenever you install or remove a plugin.

This is the actual problem. The filter hack is fine. The Google tutorials are wrong about where it goes, but right that the underlying mechanism exists. What they don't help with is everything around it: dependency safety, request classification, multilingual handling, shadow observation, telemetry, WP-CLI deployment across many sites, and the boring but critical question of "does this rule still apply after the plugin updated last night?"

I built AcceleratorWP because I got tired of writing the same 200-line mu-plugin across every client site. It does the same thing the snippet above tries to do, plus the five fixes above, plus a 300-entry rule library of safe defaults for popular plugins, plus a setup wizard that scans your stack and proposes rules instead of making you write them by hand.

That's the disclosure paragraph. The rest of this post is the technique itself, regardless of whether you use our tool or write your own.

A real example: the React-style admin UI problem

The pattern I see most often in 2026 is a custom React or Vue dashboard inside wp-admin. Filter inputs, search boxes, quick-edit, drag-to-reorder — every interaction fires an admin-ajax call. On a 50-plugin install, every one of those calls pays the full plugin boot cost. The interaction time goes from "instant" — about 30 ms server-side for what should be a 5 ms database read — to a noticeable 1.5 to 2 seconds. The user clicks, waits, gets bored, double-clicks, fires a duplicate, the whole UI feels heavy.

The naive fix is the same option_active_plugins filter, conditioned on wp_doing_ajax() && $_REQUEST['action'] === 'mydash_get_orders'. This works once. Then your security plugin (Wordfence) wants to be present on every request. The AJAX action your dashboard calls actually requires the WooCommerce session under the hood. Your rate-limiter expects to see traffic from this IP. Now you have a different bug.

What works: the request runs, the action's owning plugin stays loaded, every other plugin gets stripped from that one dispatch, and an explicit allowlist keeps a couple of essentials (security, rate limit) in place. We call this Isolate Mode. One of our beta testers measured 1.7–2.0 seconds → 170 ms on his custom React admin UI on a 50-plugin install — the same 10× improvement you'd hope for from rewriting the page in pure PHP, except you didn't rewrite anything.

If you want to implement this manually:

<?php
// In a mu-plugin, runs before WordPress loads plugins.
 
add_filter('option_active_plugins', function ($plugins) {
    if (!wp_doing_ajax()) {
        return $plugins;
    }
 
    $action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
    if ($action !== 'mydash_get_orders') {
        return $plugins;
    }
 
    // Only keep WooCommerce (the action's owner) + Wordfence (security).
    return array_intersect($plugins, [
        'woocommerce/woocommerce.php',
        'wordfence/wordfence.php',
    ]);
});

You will spend the next week debugging the parts the snippet doesn't cover. Which actions to whitelist. What to do when the action depends on capabilities provided by a plugin you stripped. What to do during shadow mode when you want to measure this without applying it. What to do when the dashboard calls an action you forgot to add to the rule. That gap — between the snippet and the production-grade version — is roughly the gap between a blog comment and a product.

Should you write your own or install something?

A reasonable test: count how many of your sites are slow.

  • One site, one plugin to skip on a couple of URLs. Write the mu-plugin yourself. It's fifty lines. You'll learn something. Total time investment is half a day. We have an installation guide you can ignore for this case.

  • Two to four sites, varying stacks. Still write it yourself, but copy the dependency graph code from somewhere reputable, and test it religiously in shadow mode for a week before going live. Document which rules apply to which site. Use version control.

  • Five or more client sites, or any site complex enough to hit two of the five footguns above. Use a tool. Ours, Plugin Organizer, Asset CleanUp, or hand-rolled with a proper internal abstraction — pick one and commit. The maintenance cost of N hand-rolled mu-plugins exceeds the price of a license very quickly.

There's a fourth case worth flagging: you have one site, but it's the one that pays your bills. WooCommerce, custom admin UI, multilingual, payment gateways. Don't write the half-baked version. Either get the snippet 100% right after shadow soak, or use a tool. The cost of getting it wrong on production is everyone's worst Monday.

What the answer looks like, regardless of the tool

Whatever path you pick, the working version of "load this plugin only on these pages" has the same shape:

  1. The code runs in the mu-plugins layer, not in functions.php. The hook is too late if it lives in your theme.
  2. Decisions are made by request class, not just URL — frontend page view vs. logged-in vs. admin vs. admin-ajax vs. REST vs. cron. Each has its own answer.
  3. Every skip is checked against a dependency graph before being applied. If skipping plugin A would break plugin B that's still active, the skip is refused at runtime.
  4. Multilingual prefixes (/tr/, /de/, /es/, etc.) are handled either programmatically or with explicit aliases. Translated slugs need an extra-targets list.
  5. New rules are observed in shadow mode for a realistic traffic window before they affect real visitors. Real traffic surfaces edge cases your local browser-tab test never will.
  6. There is some telemetry that confirms the rule actually fires, on real traffic, and that nothing exploded. Without telemetry you're guessing.

If your version of this hits all six: ship it. If it doesn't, you're going to find out the gap exists in the form of a Slack message at 2 a.m. that doesn't say "the contact form is slow." It says "the contact form is broken."

If you want to skip the year of accumulated bug reports, take a look at AcceleratorWP or run the wizard on a staging site. We do all six. The free tier covers the rule library and the Plugin Skipper. The premium adds per-URL Keep/Skip overrides, language-aware matching, and Isolate Mode for admin-ajax calls.

Either way — please don't drop the option_active_plugins filter into your functions.php and expect it to do anything. You'll check the page, the page will look fine, and the bug will live in production for six months until you're staring at Query Monitor wondering why none of this is working.

If you've already done that, no judgment. We've all done it. Move the file to mu-plugins/, refresh, look at Query Monitor again. The number should be smaller this time.

Free PDF

The 50-step WordPress performance cheatsheet.

The list I wish I'd had when I started tuning client sites — every step works without our plugin. Drop your email and the PDF lands in your inbox in under a minute. You'll also get the biweekly field notes; unsubscribe on the first click of the first email if it's not for you.

SE

Sarp Efe

Founder & developer · AcceleratorWP

Get Accelerator