Commit 0e1861682a9 for woocommerce

commit 0e1861682a97299deeec617d52dbfed5a56fca69
Author: Alba Rincón <albarin@users.noreply.github.com>
Date:   Thu Jun 4 11:40:15 2026 +0200

    Batch-prime product image caches across product collection endpoints (#65436)

    * Batch-prime product image caches in product collection endpoints

    * Use ProductUtil::prime_image_caches in CartSchema

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Use ProductUtil import alias instead of FQN

    * Shorten comment.

    * Make ProductUtil::prime_image_caches an instance method resolved via the container

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/65436-performance-batch-prime-product-collection-images b/plugins/woocommerce/changelog/65436-performance-batch-prime-product-collection-images
new file mode 100644
index 00000000000..9ee0889fd20
--- /dev/null
+++ b/plugins/woocommerce/changelog/65436-performance-batch-prime-product-collection-images
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Batch-prime product image attachment caches across the products collection endpoints (Store API, REST v3, v4) and reuse the shared helper in the Store API cart.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
index 7abd1ac3518..d4296b32f87 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
@@ -14,6 +14,7 @@ use Automattic\WooCommerce\Enums\ProductTaxStatus;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Enums\CatalogVisibility;
 use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareRestControllerTrait;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
 use Automattic\WooCommerce\Utilities\I18nUtil;
 use Automattic\WooCommerce\Utilities\MetaDataUtil;

@@ -520,6 +521,12 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
 			$this->exclude_status = array();
 		}

+		// Batch-prime image attachment caches for the whole collection, rather than once per
+		// product when get_images() runs during serialization.
+		if ( ! empty( $result['objects'] ) ) {
+			wc_get_container()->get( ProductUtil::class )->prime_image_caches( $result['objects'] );
+		}
+
 		return $result;
 	}

diff --git a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
index a851fe00518..bf4fb934f72 100644
--- a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
+++ b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
@@ -18,6 +18,7 @@ use Automattic\WooCommerce\Enums\ProductTaxStatus;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Enums\CatalogVisibility;
 use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareRestControllerTrait;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
 use Automattic\WooCommerce\Utilities\I18nUtil;
 use Automattic\WooCommerce\Utilities\MetaDataUtil;
 use WC_REST_Products_V2_Controller;
@@ -709,6 +710,12 @@ class Controller extends WC_REST_Products_V2_Controller {
 			$this->exclude_status = array();
 		}

+		// Batch-prime image attachment caches for the whole collection, rather than once per
+		// product when get_images() runs during serialization.
+		if ( ! empty( $result['objects'] ) ) {
+			wc_get_container()->get( ProductUtil::class )->prime_image_caches( $result['objects'] );
+		}
+
 		return $result;
 	}

diff --git a/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php b/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
index 885c01a37ab..0e31d4242c0 100644
--- a/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
+++ b/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
@@ -44,4 +44,22 @@ class ProductUtil {
 			}
 		}
 	}
+
+	/**
+	 * Prime featured and gallery image attachment caches for a collection of products in a single
+	 * batched query, instead of priming each product's images separately.
+	 *
+	 * @param array $products Products whose image attachments should be primed. Non-product items are ignored.
+	 * @return void
+	 */
+	public function prime_image_caches( array $products ): void {
+		$products  = array_filter( $products, static fn( $product ) => $product instanceof \WC_Product );
+		$featured  = array_map( static fn( $product ) => $product->get_image_id(), $products );
+		$gallery   = array_map( static fn( $product ) => $product->get_gallery_image_ids(), $products );
+		$image_ids = array_filter( array_unique( array_map( 'intval', array_merge( $featured, ...$gallery ) ) ) );
+		if ( ! empty( $image_ids ) ) {
+			// Prime caches to reduce future queries.
+			_prime_post_caches( $image_ids );
+		}
+	}
 }
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/CartSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/CartSchema.php
index 0f0c0303738..55b2a3cf52c 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/CartSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/CartSchema.php
@@ -1,6 +1,7 @@
 <?php
 namespace Automattic\WooCommerce\StoreApi\Schemas\V1;

+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
 use Automattic\WooCommerce\StoreApi\SchemaController;
 use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
 use Automattic\WooCommerce\StoreApi\Utilities\CartController;
@@ -343,29 +344,18 @@ class CartSchema extends AbstractSchema {
 		// Get visible cross sells products.
 		$cross_sells    = array();
 		$cross_sell_ids = $cart->get_cross_sells();
-		$image_ids      = array();
 		if ( ! empty( $cross_sell_ids ) ) {
 			// Prime caches to reduce future queries.
 			_prime_post_caches( $cross_sell_ids );
 			$cross_sells = array_values( array_filter( array_map( 'wc_get_product', $cross_sell_ids ), 'wc_products_array_filter_visible' ) );
 			/** @var \WC_Product[] $cross_sells */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
-			// Identify which images need priming.
-			$ids         = array_map( static fn( $product ) => array( (int) $product->get_image_id(), ...$product->get_gallery_image_ids() ), $cross_sells );
-			$image_ids[] = array_values( array_filter( array_merge( ...$ids ) ) );
 		}

 		$cart_all_items  = $cart->get_cart();
 		$cart_line_items = array_values( array_filter( $cart_all_items, static fn( $item ) => ( $item['data'] ?? null ) instanceof \WC_Product ) );
-		if ( ! empty( $cart_line_items ) ) {
-			// Identify which images need priming.
-			$ids         = array_map( static fn( $item ) => array( (int) $item['data']->get_image_id(), ...$item['data']->get_gallery_image_ids() ), $cart_line_items );
-			$image_ids[] = array_values( array_filter( array_merge( ...array_values( $ids ) ) ) );
-		}

-		if ( ! empty( $image_ids ) ) {
-			// Prime caches to reduce future queries.
-			_prime_post_caches( array_unique( array_merge( ...$image_ids ) ) );
-		}
+		// Batch-prime image attachment caches for cross-sells and cart line items in one query.
+		wc_get_container()->get( ProductUtil::class )->prime_image_caches( array_merge( $cross_sells, array_column( $cart_line_items, 'data' ) ) );

 		return [
 			'items'                   => $this->get_item_responses_from_schema( $this->item_schema, $cart_all_items ),
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php b/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php
index 98ef5bb250b..e838b5c683d 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php
@@ -7,6 +7,7 @@ use Automattic\WooCommerce\Enums\ProductStatus;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Enums\CatalogVisibility;
 use Automattic\WooCommerce\Internal\ProductFilters\Interfaces\QueryClausesGenerator;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
 use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
 use WC_Tax;

@@ -356,8 +357,14 @@ class ProductQuery implements QueryClausesGenerator {
 			_prime_post_caches( $results['results'] );
 		}

+		$objects = array_map( 'wc_get_product', $results['results'] );
+
+		// Batch-prime image attachment caches for the whole collection, rather than once per
+		// product when ProductSchema::get_images() runs during serialization.
+		wc_get_container()->get( ProductUtil::class )->prime_image_caches( $objects );
+
 		return array(
-			'objects' => array_map( 'wc_get_product', $results['results'] ),
+			'objects' => $objects,
 			'total'   => $results['total'],
 			'pages'   => $results['pages'],
 		);