Commit 7a9e8d454d9 for woocommerce

commit 7a9e8d454d9cd37b0ccdedc4141eb220e13640c9
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Wed Jun 24 12:22:27 2026 +0200

    [Performance] Centralize wp_count_posts invocations towards products (#65894)

    Groundwork for optimizing admin performance with big product catalogs: centralize wp_count_posts calls via a new utility class. Reduces the number of integration points in subsequent optimization steps.

diff --git a/plugins/woocommerce/changelog/performance-centralize-wp_count_posts-invocation b/plugins/woocommerce/changelog/performance-centralize-wp_count_posts-invocation
new file mode 100644
index 00000000000..f3d5d9813d5
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-centralize-wp_count_posts-invocation
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Centralize wp_count_posts invocations towards products.
diff --git a/plugins/woocommerce/includes/admin/list-tables/abstract-class-wc-admin-list-table.php b/plugins/woocommerce/includes/admin/list-tables/abstract-class-wc-admin-list-table.php
index 4a0c93d6274..6939d4b8ce0 100644
--- a/plugins/woocommerce/includes/admin/list-tables/abstract-class-wc-admin-list-table.php
+++ b/plugins/woocommerce/includes/admin/list-tables/abstract-class-wc-admin-list-table.php
@@ -63,10 +63,9 @@ abstract class WC_Admin_List_Table {

 		if ( $post_type === $this->list_table_type && 'bottom' === $which ) {
 			$counts = (array) wp_count_posts( $post_type );
-			unset( $counts['auto-draft'] );
-			$count = array_sum( $counts );
+			$count  = array_sum( $counts ) - ( $counts['auto-draft'] ?? 0 );

-			if ( 0 < $count ) {
+			if ( $count > 0 ) {
 				return;
 			}

diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index e2fba02952b..8ae2a7eb9ca 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -21,6 +21,7 @@ use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersSta
 use Automattic\WooCommerce\Utilities\FeaturesUtil;
 use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
 use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper as WCConnectionHelper;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
 use Automattic\WooCommerce\Utilities\{ OrderUtil, PluginUtil };

 defined( 'ABSPATH' ) || exit;
@@ -851,7 +852,7 @@ class WC_Install {
 		return is_null( get_option( 'woocommerce_version', null ) )
 			|| (
 				-1 === wc_get_page_id( 'shop' )
-				&& 0 === array_sum( (array) wp_count_posts( 'product' ) )
+				&& 0 === array_sum( ProductUtil::get_counts_for_type( 'product' ) )
 			);
 	}

diff --git a/plugins/woocommerce/includes/class-wc-tracker.php b/plugins/woocommerce/includes/class-wc-tracker.php
index 432e1d3c966..9930b2eaf19 100644
--- a/plugins/woocommerce/includes/class-wc-tracker.php
+++ b/plugins/woocommerce/includes/class-wc-tracker.php
@@ -11,14 +11,16 @@
  */

 use Automattic\Jetpack\Constants;
+use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
+use Automattic\WooCommerce\Blocks\Package;
+use Automattic\WooCommerce\Enums\ProductStatus;
 use Automattic\WooCommerce\Internal\Admin\EmailImprovements\EmailImprovements;
 use Automattic\WooCommerce\Internal\CLI\Migrator\Core\MigratorTracker;
 use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
-use Automattic\WooCommerce\Utilities\{ FeaturesUtil, OrderUtil, PluginUtil };
 use Automattic\WooCommerce\Internal\Utilities\BlocksUtil;
 use Automattic\WooCommerce\Proxies\LegacyProxy;
-use Automattic\WooCommerce\Blocks\Package;
-use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
+use Automattic\WooCommerce\Utilities\{ FeaturesUtil, OrderUtil, PluginUtil };

 defined( 'ABSPATH' ) || exit;

@@ -488,9 +490,8 @@ class WC_Tracker {
 	 * @return array
 	 */
 	public static function get_product_counts() {
-		$product_count          = array();
-		$product_count_data     = wp_count_posts( 'product' );
-		$product_count['total'] = $product_count_data->publish;
+		$product_count_data = ProductUtil::get_counts_for_type( 'product' );
+		$product_count      = array( 'total' => $product_count_data[ ProductStatus::PUBLISH ] ?? 0 );

 		$product_statuses = get_terms( 'product_type', array( 'hide_empty' => 0 ) );
 		foreach ( $product_statuses as $product_status ) {
diff --git a/plugins/woocommerce/src/Admin/API/OnboardingTasks.php b/plugins/woocommerce/src/Admin/API/OnboardingTasks.php
index 8d37abb2a1c..3b5c75e31fb 100644
--- a/plugins/woocommerce/src/Admin/API/OnboardingTasks.php
+++ b/plugins/woocommerce/src/Admin/API/OnboardingTasks.php
@@ -7,12 +7,13 @@

 namespace Automattic\WooCommerce\Admin\API;

+use Automattic\WooCommerce\Admin\Features\Features;
+use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedExtendedTask;
+use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
 use Automattic\WooCommerce\Enums\ProductStatus;
 use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingIndustries;
 use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
-use Automattic\WooCommerce\Admin\Features\Features;
-use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
-use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedExtendedTask;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;

 defined( 'ABSPATH' ) || exit;

@@ -512,8 +513,8 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
 	 * @return string Template contents.
 	 */
 	private static function get_homepage_template( $post_id ) {
-		$products = wp_count_posts( 'product' );
-		if ( $products->publish >= 4 ) {
+		$products = ProductUtil::get_counts_for_type( 'product' );
+		if ( ( $products[ ProductStatus::PUBLISH ] ?? 0 ) >= 4 ) {
 			$images   = self::sideload_homepage_images( $post_id, 1 );
 			$image_1  = ! empty( $images[0] ) ? $images[0] : '';
 			$template = self::get_homepage_cover_block( $image_1 ) . '
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php
index edb3b05320b..ff7d22945da 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php
@@ -4,8 +4,9 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

 use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
 use Automattic\WooCommerce\Enums\ProductStatus;
-use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
 use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
+use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;

 /**
  * Products Task
@@ -339,10 +340,8 @@ class Products extends Task {
 	public function maybe_redirect_to_add_product_tasklist() {
 		$screen = get_current_screen();
 		if ( $screen && 'edit' === $screen->base && 'product' === $screen->post_type ) {
-			// wp_count_posts is cached.
-			$counts = (array) wp_count_posts( $screen->post_type );
-			unset( $counts['auto-draft'] );
-			$count = array_sum( $counts );
+			$counts = ProductUtil::get_counts_for_type( 'product' );
+			$count  = array_sum( $counts ) - ( $counts[ ProductStatus::AUTO_DRAFT ] ?? 0 );
 			if ( $count > 0 ) {
 				return;
 			}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
index 336666503de..404da7ffe83 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
@@ -1,12 +1,13 @@
 <?php
 namespace Automattic\WooCommerce\Blocks\BlockTypes;

-use WP_Block;
-use Automattic\WooCommerce\Blocks\Package;
-use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
+use Automattic\WooCommerce\Admin\Features\Features;
 use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
+use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
 use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
-use Automattic\WooCommerce\Admin\Features\Features;
+use Automattic\WooCommerce\Enums\ProductStatus;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
+use WP_Block;

 /**
  * AbstractBlock class.
@@ -443,8 +444,8 @@ abstract class AbstractBlock {
 				'wordCountType' => _x( 'words', 'Word count type. Do not translate!', 'woocommerce' ),
 			];
 			if ( is_admin() && ! WC()->is_rest_api_request() ) {
-				$product_counts     = wp_count_posts( 'product' );
-				$published_products = isset( $product_counts->publish ) ? $product_counts->publish : 0;
+				$product_counts     = ProductUtil::get_counts_for_type( 'product' );
+				$published_products = $product_counts[ ProductStatus::PUBLISH ] ?? 0;
 				$wc_blocks_config   = array_merge(
 					$wc_blocks_config,
 					[
diff --git a/plugins/woocommerce/src/Internal/Admin/SiteHealth.php b/plugins/woocommerce/src/Internal/Admin/SiteHealth.php
index afb182a24f6..ff8ee30b9e6 100644
--- a/plugins/woocommerce/src/Internal/Admin/SiteHealth.php
+++ b/plugins/woocommerce/src/Internal/Admin/SiteHealth.php
@@ -8,7 +8,9 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Internal\Admin;

 use Automattic\WooCommerce\Enums\DefaultCustomerAddress;
+use Automattic\WooCommerce\Enums\ProductStatus;
 use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
 use Automattic\WooCommerce\Utilities\OrderUtil;
 use WC_Admin_Notices;
 use WC_Admin_Status;
@@ -680,9 +682,9 @@ class SiteHealth {
 			return false;
 		}

-		$product_count = wp_count_posts( 'product' );
+		$product_count = ProductUtil::get_counts_for_type( 'product' );

-		return $product_count->publish > 0 && 0 === wc_get_shipping_method_count();
+		return ( $product_count[ ProductStatus::PUBLISH ] ?? 0 ) > 0 && 0 === wc_get_shipping_method_count();
 	}

 	/**
diff --git a/plugins/woocommerce/src/Internal/ProductAttributesLookup/CLIRunner.php b/plugins/woocommerce/src/Internal/ProductAttributesLookup/CLIRunner.php
index 158ed088908..8463ffca414 100644
--- a/plugins/woocommerce/src/Internal/ProductAttributesLookup/CLIRunner.php
+++ b/plugins/woocommerce/src/Internal/ProductAttributesLookup/CLIRunner.php
@@ -2,6 +2,8 @@

 namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;

+use Automattic\WooCommerce\Enums\ProductStatus;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
 use WP_CLI;

 /**
@@ -382,10 +384,8 @@ class CLIRunner {

 		$was_enabled = 'yes' === get_option( 'woocommerce_attribute_lookup_enabled' );

-		// phpcs:ignore Generic.Commenting.Todo.TaskFound
-		// TODO: adjust for non-CPT datastores (this is only used for the progress bar, though).
-		$products_count = wp_count_posts( 'product' );
-		$products_count = intval( $products_count->publish ) + intval( $products_count->pending ) + intval( $products_count->draft );
+		$products_count = ProductUtil::get_counts_for_type( 'product' );
+		$products_count = ( $products_count[ ProductStatus::PUBLISH ] ?? 0 ) + ( $products_count[ ProductStatus::PENDING ] ?? 0 ) + ( $products_count[ ProductStatus::DRAFT ] ?? 0 );

 		if ( ! $this->lookup_data_store->regeneration_is_in_progress() || array_key_exists( 'from-scratch', $assoc_args ) ) {
 			$info = $this->get_lookup_table_info();
diff --git a/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php b/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
index c925cf0eeb6..503504b151e 100644
--- a/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
+++ b/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
@@ -127,4 +127,17 @@ class ProductUtil {
 			_prime_post_caches( $image_ids );
 		}
 	}
+
+	/**
+	 * Counts per-status number of products.
+	 *
+	 * @since 11.0.0
+	 *
+	 * @param string $post_type Post type (e.g. 'product', 'product_variation').
+	 * @return array<string,int>
+	 */
+	public static function get_counts_for_type( string $post_type ): array {
+		// Performance note: integration point for upcoming persistent counters solution.
+		return array_map( 'intval', (array) wp_count_posts( $post_type ) );
+	}
 }
diff --git a/plugins/woocommerce/src/Utilities/OrderUtil.php b/plugins/woocommerce/src/Utilities/OrderUtil.php
index e2d5a6811ea..dbe798c24e8 100644
--- a/plugins/woocommerce/src/Utilities/OrderUtil.php
+++ b/plugins/woocommerce/src/Utilities/OrderUtil.php
@@ -231,18 +231,15 @@ final class OrderUtil {

 		if ( null === $count_per_status ) {
 			if ( self::custom_orders_table_usage_is_enabled() ) {
-				// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
-				$results = $wpdb->get_results(
+				$results          = $wpdb->get_results(
 					$wpdb->prepare(
-						'SELECT `status`, COUNT(*) AS `count` FROM ' . self::get_table_for_orders() . ' WHERE `type` = %s GROUP BY `status`',
+						'SELECT status, COUNT(*) AS count FROM %i WHERE type = %s GROUP BY status',
+						self::get_table_for_orders(),
 						$order_type
 					),
 					ARRAY_A
 				);
-				// phpcs:enable
-
 				$count_per_status = array_map( 'absint', array_column( $results, 'count', 'status' ) );
-
 			} else {
 				$count_per_status = (array) wp_count_posts( $order_type );
 			}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Utilities/ProductUtilTest.php b/plugins/woocommerce/tests/php/src/Internal/Utilities/ProductUtilTest.php
index ab28f843873..7be7ceb010a 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Utilities/ProductUtilTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Utilities/ProductUtilTest.php
@@ -4,13 +4,35 @@ declare(strict_types=1);

 namespace Automattic\WooCommerce\Tests\Internal\Utilities;

-use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;
+use Automattic\WooCommerce\Enums\ProductStatus;
 use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
+use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;

 /**
  * Tests for the internal ProductUtil class.
  */
 class ProductUtilTest extends \WC_Unit_Test_Case {
+	/**
+	 * @testdox `get_counts_for_type` returns per-status counts for the given post type.
+	 */
+	public function test_get_counts_for_type_returns_per_status_counts(): void {
+		$before = ProductUtil::get_counts_for_type( 'product' );
+
+		$published = \WC_Helper_Product::create_simple_product();
+		$draft     = \WC_Helper_Product::create_simple_product( true, array( 'status' => ProductStatus::DRAFT ) );
+		$pending   = \WC_Helper_Product::create_simple_product( true, array( 'status' => ProductStatus::PENDING ) );
+
+		$after = ProductUtil::get_counts_for_type( 'product' );
+
+		$this->assertSame( ( $before[ ProductStatus::PUBLISH ] ?? 0 ) + 1, $after[ ProductStatus::PUBLISH ] );
+		$this->assertSame( ( $before[ ProductStatus::DRAFT ] ?? 0 ) + 1, $after[ ProductStatus::DRAFT ] );
+		$this->assertSame( ( $before[ ProductStatus::PENDING ] ?? 0 ) + 1, $after[ ProductStatus::PENDING ] );
+
+		$published->delete( true );
+		$draft->delete( true );
+		$pending->delete( true );
+	}
+
 	/**
 	 * @testdox delete_product_transients_for_products deletes fixed-name transients once and fires hooks once per product.
 	 */