BEAR - Bulk Editor and Products Manager Professional for WooCommerce

WordPress plugin conflicts and a slow WooCommerce admin: isolating plugins during bulk edits

A WooCommerce plugin conflict rarely announces itself. On a shop with sixty active plugins the symptom is a slow WooCommerce admin and a bulk operation that behaves differently than on a clean install: it drags, sometimes stops halfway, occasionally writes part of the products and reports an error on the rest. The bulk editor is rarely the cause. Every plugin on the site hangs its own callbacks on save_post and woocommerce_update_product, and when BEAR saves three thousand products, all of those callbacks run three thousand times each. A page builder regenerating its cache, an SEO plugin recalculating scores, a search plugin reindexing, a sync plugin calling an external API: none of them were written with a bulk write in mind.

Too many plugins on a WordPress site is not a problem you solve by deactivating them, because you need them the rest of the day. It is a problem you solve for one request at a time.

WordPress has one place where this can be controlled, and it is earlier than most people expect.

Why a WordPress plugin conflict cannot be fixed from functions.php

The usual advice for a WordPress plugin conflict is to deactivate plugins one by one until the symptom disappears. That finds the culprit and leaves you with a choice you do not want to make, because the plugin you have to keep off is also a plugin the shop needs. WordPress loads plugins before it loads the theme. By the time functions.php runs, every plugin file has been included and every hook is already registered. Removing callbacks at that point means chasing them one by one with remove_action, and you will always miss some: closures cannot be removed by name, priorities differ, and a plugin that registers its hooks lazily will re-add them after you are done. The result is a half-disabled plugin, which is worse than a fully loaded one.

There is one hook that runs before plugins load. WordPress reads the list of active plugins from the active_plugins option and includes each file in that list. The option passes through the option_active_plugins filter first. Return a shorter array there and the other plugin files are never read at all. Not disabled, not silenced: never loaded.

Code that must run before plugins load cannot itself live in a plugin. It lives in wp-content/mu-plugins/. WordPress must-use plugins are loaded before anything in the regular plugin list, they cannot be deactivated from the admin, and every PHP file placed directly in that folder runs automatically. That is early enough for the filter above to still matter.

WordPress must-use plugins load first

Save this as wp-content/mu-plugins/bear-isolation.php. If the WordPress mu plugins folder does not exist on your site yet, create it: it is an ordinary directory inside wp-content, and every PHP file in it is loaded automatically. There is nothing to activate afterwards.

<?php
/**
 * Plugin Name: BEAR Plugin Isolation
 * Description: Loads a reduced set of plugins during BEAR bulk editor and MCP requests.
 * Version: 1.0
 *
 * Place this file into wp-content/mu-plugins/
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Mode:
 *   'disable'   - load everything EXCEPT the plugins listed below. Safer, start here.
 *   'keep_only' - load ONLY the plugins listed below. Fastest, needs testing.
 */
define( 'BEAR_ISOLATION_MODE', 'disable' );

/**
 * The list the mode above applies to. Paths are exactly as they appear
 * in the active_plugins option: folder/file.php
 */
function bear_isolation_list() {

	$list = array(
		// 'elementor/elementor.php',
		// 'js_composer/js_composer.php',
		// 'contact-form-7/wp-contact-form-7.php',
		// 'wp-smushit/wp-smush.php',
	);

	return apply_filters( 'bear_isolation_list', $list );
}

/**
 * Plugins that stay loaded whatever the mode and the list say.
 * Without WooCommerce and BEAR itself the request cannot work at all.
 */
function bear_isolation_always_keep() {

	$keep = array(
		'woocommerce/woocommerce.php',
		'woo-bulk-editor/index.php',   // same folder for the free and the paid build
	);

	return apply_filters( 'bear_isolation_always_keep', $keep );
}

/**
 * Is this a request the isolation should apply to?
 *
 * REST routing has not run yet at this point, so the raw URI is the only
 * signal available for the MCP endpoint.
 */
function bear_isolation_is_target_request() {

	$uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : '';

	// The MCP endpoint: pretty permalinks and the plain query form.
	if ( strpos( $uri, '/woobe/v1/mcp' ) !== false
		|| strpos( $uri, 'rest_route=/woobe/v1/mcp' ) !== false ) {
		return true;
	}

	// The bulk editor admin page.
	if ( isset( $_GET['page'] ) && strpos( (string) $_GET['page'], 'woobe' ) === 0 ) {
		return true;
	}

	// The editor's own ajax calls: woobe_get_products, woobe_bulk_edit and the rest.
	if ( isset( $_REQUEST['action'] ) && strpos( (string) $_REQUEST['action'], 'woobe' ) === 0 ) {
		return true;
	}

	return false;
}

/**
 * Regular single-site activation list: values are plugin paths.
 */
function bear_isolation_filter_plugins( $plugins ) {

	if ( ! is_array( $plugins ) || ! bear_isolation_is_target_request() ) {
		return $plugins;
	}

	$keep = bear_isolation_always_keep();
	$list = bear_isolation_list();

	if ( BEAR_ISOLATION_MODE === 'keep_only' ) {
		$allowed = array_merge( $keep, $list );
		return array_values( array_intersect( $plugins, $allowed ) );
	}

	// 'disable': drop the listed ones, but never the ones that must stay.
	$drop = array_diff( $list, $keep );
	return array_values( array_diff( $plugins, $drop ) );
}

add_filter( 'option_active_plugins', 'bear_isolation_filter_plugins', 1 );

/**
 * Multisite: network-activated plugins live in another option, and there the
 * plugin path is the KEY, not the value. Hence the _key variants.
 */
function bear_isolation_filter_network_plugins( $plugins ) {

	if ( ! is_array( $plugins ) || ! bear_isolation_is_target_request() ) {
		return $plugins;
	}

	$keep = bear_isolation_always_keep();
	$list = bear_isolation_list();

	if ( BEAR_ISOLATION_MODE === 'keep_only' ) {
		$allowed = array_flip( array_merge( $keep, $list ) );
		return array_intersect_key( $plugins, $allowed );
	}

	$drop = array_flip( array_diff( $list, $keep ) );
	return array_diff_key( $plugins, $drop );
}

add_filter( 'site_option_active_sitewide_plugins', 'bear_isolation_filter_network_plugins', 1 );

The two modes

disable is the one to start with. Everything loads as usual except the plugins you name. Add the heavy ones that have no business in a product write: page builders, sliders, galleries, form plugins, analytics, chat widgets, image optimisers, backup plugins. Each one you add is one less set of callbacks running per product.

keep_only is the opposite and the faster of the two: nothing loads except WooCommerce, BEAR and whatever you list. On a site with sixty plugins this turns a bulk write into something close to a clean install. It also removes plugins you may actually need during the write, so treat the list as something you build up by testing, not by guessing.

Both modes protect WooCommerce and BEAR through bear_isolation_always_keep(), so a typo in the list cannot break the request. The plugin folder is woo-bulk-editor for both the free and the paid build, so the path above is the same whichever one you run. If you are unsure about the path of any other plugin, it is visible in Plugins in wp-admin, or in the active_plugins option.

What the isolation covers

Three kinds of request, and you can narrow them by editing bear_isolation_is_target_request():

  • The MCP endpoint, /wp-json/woobe/v1/mcp and its ?rest_route= form. This is where an AI assistant writes products, and where a plugin conflict is hardest to notice, because nobody is looking at the screen.
  • The bulk editor page itself, edit.php?post_type=product&page=woobe. This is the one that loads visibly faster: the admin no longer pulls in every metabox and every admin script of every plugin.
  • The editor's ajax calls, every action starting with woobe. This is where the actual writing happens, so this is where the callbacks would have fired.

If you want the isolation only for MCP and not for the editor page, delete the second and third check. The rest of the file keeps working.

What you lose, and this is the part to think about

A plugin that is not loaded does not react to a product being saved. Most of the time that is exactly the point. Sometimes it is a problem, and it is better to know which before running an operation on the whole catalogue.

Search and sync. Relevanssi, ElasticPress, Algolia, feed generators, ERP connectors: they update their index or push data on save_post. Isolate them and three thousand products change in the database while the index still describes the old ones. Rebuild the index afterwards, or leave these plugins loaded.

Multilingual. WPML and Polylang create and link translations when a product is saved. Without them the write lands only in the source language and the connections between translations can drift.

Fields defined by other plugins. ACF, Subscriptions, Bookings, custom metaboxes: if the plugin is not loaded, its fields are not registered, and BEAR does not show them as columns. The operation will not corrupt anything, but a field you expected to edit will not be there.

Caches. Cache plugins invalidate pages on save. Isolated, they will not, and the shop front keeps serving the old prices until the cache expires or you purge it by hand.

Security plugins. A firewall plugin in the disable list is a firewall not running on that request. The MCP endpoint has its own token and, with two-factor connection enabled, its own second check, but this is a decision to make deliberately rather than by accident.

Two things this method cannot switch off at all: other mu-plugins, and the active theme. Both load outside active_plugins. If your theme's functions.php carries the heaviest code on the site, the isolation will not help with that part.

Testing it before you trust it

Work on a staging copy first, with a small filter rather than the whole catalogue.

  1. Install the file with an empty list and confirm nothing changes. The site must behave exactly as before.
  2. Add one plugin to the list, open the bulk editor page and check the columns you normally use are still there.
  3. Run a bulk operation on ten products and compare the result with what you expect: prices written, stock written, the shop front showing the new values after a cache purge.
  4. Add the next plugin and repeat. When something disappears or stops updating, the last plugin you added is the reason.

To remove the isolation entirely, delete the file. There is nothing to deactivate and nothing left behind in the database.

When this is worth doing: a slow WooCommerce admin, and when it is not

On a shop with a dozen plugins, no. WordPress handles that fine and the gain is not worth the file. It earns its place where the plugin list runs long, where a bulk operation over thousands of products times out or finishes partially, or where an AI assistant writes through the MCP server and nobody is watching the screen to notice that a sync plugin fired eight thousand API calls in the background.

It is also the honest answer to "how do I speed up the WooCommerce admin" when the real reason is the number of plugins rather than the server: instead of hunting for the one plugin to blame, you decide which ones have any business being loaded while products are written.