Commit 362f1b3fcad for woocommerce

commit 362f1b3fcad0ee716a8f3ee99096cd1258b7c5c4
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Fri Oct 2 08:52:43 2026 +0200

    Performance: bulk-prime variable products transients on shop and category pages to reduce the number of SQL queries. (#69319)

    Performs bulk priming of variable product transients on the shop and category pages.
    Collapses N individual priming SQLs into a single one covering all variable products on the page.

diff --git a/plugins/woocommerce/changelog/performance-shop-page-variable-products-sqls-count-reduction b/plugins/woocommerce/changelog/performance-shop-page-variable-products-sqls-count-reduction
new file mode 100644
index 00000000000..b53c0af9ef3
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-shop-page-variable-products-sqls-count-reduction
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Bulk-prime variable products transients on shop and category pages to reduce the number of SQL queries.
diff --git a/plugins/woocommerce/includes/class-wc-query.php b/plugins/woocommerce/includes/class-wc-query.php
index b35222a72c6..acea738d142 100644
--- a/plugins/woocommerce/includes/class-wc-query.php
+++ b/plugins/woocommerce/includes/class-wc-query.php
@@ -10,6 +10,7 @@ use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer;
 use Automattic\WooCommerce\Internal\ProductFilters\Params;
 use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
 use Automattic\WooCommerce\Enums\ProductStockStatus;
+use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Enums\TaxDisplayMode;

 defined( 'ABSPATH' ) || exit;
@@ -475,6 +476,7 @@ class WC_Query {
 	 * queries during rendering.
 	 *
 	 * @since 10.8.0
+	 * @internal
 	 *
 	 * @param array    $posts Posts from WP Query.
 	 * @param WP_Query $query Current query.
@@ -487,6 +489,48 @@ class WC_Query {
 		return $posts;
 	}

+	/**
+	 * Prime transient option caches for variable products in bulk to avoid per-product queries during rendering.
+	 *
+	 * @since 11.3.0
+	 * @internal
+	 *
+	 * @param array    $posts Posts from WP Query.
+	 * @param WP_Query $query Current query.
+	 * @return array
+	 */
+	public function prime_variable_products_transients( $posts, $query ) {
+		if ( 'product_query' === $query->get( 'wc_query' ) && ! wp_using_ext_object_cache() ) {
+			$options = array();
+			// Performance note: post caches has been already primed, hence the loop operates on in-memory data.
+			foreach ( $posts as $post ) {
+				$terms = $post instanceof \WP_Post ? get_object_term_cache( $post->ID, 'product_type' ) : null;
+				$terms = array_filter( is_array( $terms ) ? $terms : array(), static fn ( $term ) => $term instanceof \WP_Term ); // @phpstan-ignore instanceof.alwaysTrue (defensive checks against filters)
+				if ( ! empty( $terms ) && ProductType::VARIABLE === current( $terms )->slug ) {
+					$product_id = $post->ID;
+
+					// See also \WC_Product_Variable_Data_Store_CPT::read_product_data and sync with it when necessary.
+					$options[] = '_transient_wc_var_prices_' . $product_id;
+					$options[] = '_transient_timeout_wc_var_prices_' . $product_id;
+					$options[] = '_transient_wc_product_children_' . $product_id;
+					$options[] = '_transient_timeout_wc_product_children_' . $product_id;
+					$options[] = '_transient_wc_child_has_weight_' . $product_id;
+					$options[] = '_transient_timeout_wc_child_has_weight_' . $product_id;
+					$options[] = '_transient_wc_child_has_dimensions_' . $product_id;
+					$options[] = '_transient_timeout_wc_child_has_dimensions_' . $product_id;
+					$options[] = '_transient_wc_related_' . $product_id;
+					$options[] = '_transient_timeout_wc_related_' . $product_id;
+				}
+			}
+
+			if ( ! empty( $options ) ) {
+				wp_prime_option_caches( $options );
+			}
+		}
+
+		return $posts;
+	}
+
 	/**
 	 * Pre_get_posts above may adjust the main query to add WooCommerce logic. When this query is done, we need to ensure
 	 * all custom filters are removed.
@@ -601,6 +645,7 @@ class WC_Query {
 		add_filter( 'posts_clauses', array( $this, 'product_query_post_clauses' ), 10, 2 );
 		add_filter( 'the_posts', array( $this, 'handle_get_posts' ), 10, 2 );
 		add_filter( 'the_posts', array( $this, 'prime_thumbnail_caches' ), 10, 2 );
+		add_filter( 'the_posts', array( $this, 'prime_variable_products_transients' ), 10, 2 );

 		do_action( 'woocommerce_product_query', $q, $this );
 	}