Commit 69ac95a31ab for woocommerce
commit 69ac95a31abd9a6e058643b78ef18f40a362457c
Author: Thomas Roberts <5656702+opr@users.noreply.github.com>
Date: Fri Mar 20 14:05:17 2026 +0000
Add new thumbnail srcset and sizes properties to ImageAttachmentSchema (#63731)
* Fix Store API image srcset using wrong size for candidate generation
wp_get_attachment_image_srcset with 'full' produces candidates matching
the full image's aspect ratio. Since thumbnail is a square crop,
the square candidates (150x150, 300x300) were excluded from srcset,
making it useless for consumers that render the square thumbnail.
Switch to 'woocommerce_thumbnail' so srcset candidates share the
same aspect ratio as the thumbnail that's actually displayed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add thumbnail_srcset and thumbnail_sizes to Store API image schema
The existing srcset/sizes fields use 'full' as the reference size,
producing candidates that match the full image's aspect ratio. Since
the thumbnail is a square crop, these candidates have a different
aspect ratio and are not useful for contexts that render the thumbnail.
Add separate thumbnail_srcset and thumbnail_sizes fields generated
from 'woocommerce_thumbnail', so consumers displaying thumbnails
(cart, mini-cart, checkout) get srcset candidates with matching
aspect ratios. The original srcset/sizes fields remain unchanged
for consumers that display full-size images (product gallery).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add thumbnail_srcset and thumbnail_sizes to cart image types
Update CartImageItem, CartResponseImageItem, and CartItemImage JSDoc
to include the new fields from the Store API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Wire thumbnail_srcset into ProductImage for cart and checkout blocks
Pass thumbnail_srcset through to the img element and compute sizes
from the display width prop so the browser picks an appropriately
sized candidate. Pass explicit 80x80 dimensions in the cart block
and the checkout already passes 48x48.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Wire thumbnail_srcset into mini-cart block images
Add itemSrcset and itemSizes getters using thumbnail_srcset and bind
them to img elements in the mini-cart PHP template. Uses sizes=64px
to match the mini-cart's display size.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix lint issue
* Add changefile(s) from automation for the following project(s): woocommerce
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/63731-fix-image-srcsets b/plugins/woocommerce/changelog/63731-fix-image-srcsets
new file mode 100644
index 00000000000..a745627a6eb
--- /dev/null
+++ b/plugins/woocommerce/changelog/63731-fix-image-srcsets
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add smaller image options for product images in srcset to reduce bandwidth/load time on cart/checkout pages
\ No newline at end of file
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
index abb2ff78164..a05ab74801f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
@@ -232,12 +232,16 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
<ProductImage
image={ firstImage }
fallbackAlt={ name }
+ width={ 80 }
+ height={ 80 }
/>
) : (
<a href={ permalink } tabIndex={ -1 }>
<ProductImage
image={ firstImage }
fallbackAlt={ name }
+ width={ 80 }
+ height={ 80 }
/>
</a>
) }
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-image/index.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-image/index.tsx
index af6dc1ad95d..a0d84d8f8ba 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-image/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/product-image/index.tsx
@@ -5,7 +5,12 @@ import { decodeEntities } from '@wordpress/html-entities';
import { PLACEHOLDER_IMG_SRC } from '@woocommerce/settings';
interface ProductImageProps {
- image: { alt?: string; thumbnail?: string };
+ image: {
+ alt?: string;
+ thumbnail?: string;
+ thumbnail_srcset?: string;
+ thumbnail_sizes?: string;
+ };
fallbackAlt: string;
width?: number;
height?: number;
@@ -25,20 +30,33 @@ const ProductImage = ( {
}: ProductImageProps ): JSX.Element => {
const rawAlt = image.alt || fallbackAlt;
+ // Use display width for sizes so the browser picks an appropriately
+ // sized source from the thumbnail srcset.
+ let sizesAttr;
+ if ( image.thumbnail_srcset ) {
+ sizesAttr = width ? `${ width }px` : '100px';
+ }
+
const imageProps = image.thumbnail
? {
src: image.thumbnail,
alt: rawAlt ? decodeEntities( rawAlt ) : 'Product Image',
+ srcSet: image.thumbnail_srcset || undefined,
+ sizes: sizesAttr,
}
: {
src: PLACEHOLDER_IMG_SRC,
alt: '',
+ srcSet: undefined,
+ sizes: undefined,
};
return (
<img
src={ imageProps.src }
alt={ imageProps.alt }
+ srcSet={ imageProps.srcSet }
+ sizes={ imageProps.sizes }
width={ width }
height={ height }
/>
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
index dbf28c9a39e..c09d3a202d8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
@@ -594,6 +594,18 @@ const { state: cartItemState } = store(
);
},
+ get itemSrcset(): string {
+ return (
+ cartItemState.cartItem.images[ 0 ]?.thumbnail_srcset || ''
+ );
+ },
+
+ get itemSizes(): string {
+ return cartItemState.cartItem.images[ 0 ]?.thumbnail_srcset
+ ? '64px'
+ : '';
+ },
+
get priceWithoutDiscount(): string {
const { raw_prices: rawPrices } = cartItemState.cartItem.prices;
const priceWithoutDiscount = scalePrice( {
diff --git a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart-response.ts b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart-response.ts
index 1fbace3e932..8f8d652e95e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart-response.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart-response.ts
@@ -86,6 +86,8 @@ export interface CartResponseImageItem {
thumbnail: string;
srcset: string;
sizes: string;
+ thumbnail_srcset: string;
+ thumbnail_sizes: string;
name: string;
alt: string;
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart.js b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart.js
index 514168bb373..67149a1bf0c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart.js
+++ b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart.js
@@ -17,8 +17,10 @@
* @property {number} id Image id.
* @property {string} src Full size image URL.
* @property {string} thumbnail Thumbnail URL.
- * @property {string} srcset Thumbnail srcset for responsive image.
- * @property {string} sizes Thumbnail sizes for responsive images.
+ * @property {string} srcset Full size image srcset for responsive images.
+ * @property {string} sizes Full size image sizes for responsive images.
+ * @property {string} thumbnail_srcset Thumbnail srcset for responsive images.
+ * @property {string} thumbnail_sizes Thumbnail sizes for responsive images.
* @property {string} name Image name.
* @property {string} alt Image alternative text.
*/
diff --git a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart.ts b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart.ts
index 97287d3bda9..dc2576d53d0 100644
--- a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/cart.ts
@@ -93,6 +93,8 @@ export interface CartImageItem {
thumbnail: string;
srcset: string;
sizes: string;
+ thumbnail_srcset: string;
+ thumbnail_sizes: string;
name: string;
alt: string;
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php
index 050c626ca40..09c2a19a24f 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCartProductsTableBlock.php
@@ -119,8 +119,10 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
<td data-wp-context='{ "isImageHidden": false }' class="wc-block-cart-item__image" aria-hidden="true">
<img
data-wp-bind--hidden="!state.isProductHiddenFromCatalog"
- data-wp-bind--src="state.itemThumbnail"
+ data-wp-bind--src="state.itemThumbnail"
data-wp-bind--alt="state.cartItemName"
+ data-wp-bind--srcset="state.itemSrcset"
+ data-wp-bind--sizes="state.itemSizes"
data-wp-on--error="actions.hideImage"
>
<a data-wp-bind--hidden="state.isProductHiddenFromCatalog" data-wp-bind--href="state.cartItem.permalink" tabindex="-1">
@@ -128,6 +130,8 @@ class MiniCartProductsTableBlock extends AbstractInnerBlock {
data-wp-bind--hidden="context.isImageHidden"
data-wp-bind--src="state.itemThumbnail"
data-wp-bind--alt="state.cartItemName"
+ data-wp-bind--srcset="state.itemSrcset"
+ data-wp-bind--sizes="state.itemSizes"
data-wp-on--error="actions.hideImage"
>
</a>
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/ImageAttachmentSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/ImageAttachmentSchema.php
index 30b0465962e..e686b47ebf8 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/ImageAttachmentSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/ImageAttachmentSchema.php
@@ -26,39 +26,49 @@ class ImageAttachmentSchema extends AbstractSchema {
*/
public function get_properties() {
return [
- 'id' => [
+ 'id' => [
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'src' => [
+ 'src' => [
'description' => __( 'Full size image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'thumbnail' => [
+ 'thumbnail' => [
'description' => __( 'Thumbnail URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'srcset' => [
+ 'srcset' => [
+ 'description' => __( 'Full size image srcset for responsive images.', 'woocommerce' ),
+ 'type' => 'string',
+ 'context' => [ 'view', 'edit', 'embed' ],
+ ],
+ 'sizes' => [
+ 'description' => __( 'Full size image sizes for responsive images.', 'woocommerce' ),
+ 'type' => 'string',
+ 'context' => [ 'view', 'edit', 'embed' ],
+ ],
+ 'thumbnail_srcset' => [
'description' => __( 'Thumbnail srcset for responsive images.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'sizes' => [
+ 'thumbnail_sizes' => [
'description' => __( 'Thumbnail sizes for responsive images.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'name' => [
+ 'name' => [
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'alt' => [
+ 'alt' => [
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
@@ -86,13 +96,15 @@ class ImageAttachmentSchema extends AbstractSchema {
$thumbnail = wp_get_attachment_image_src( $attachment_id, 'woocommerce_thumbnail' );
return (object) [
- 'id' => (int) $attachment_id,
- 'src' => current( $attachment ),
- 'thumbnail' => current( $thumbnail ),
- 'srcset' => (string) wp_get_attachment_image_srcset( $attachment_id, 'full' ),
- 'sizes' => (string) wp_get_attachment_image_sizes( $attachment_id, 'full' ),
- 'name' => get_the_title( $attachment_id ),
- 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
+ 'id' => (int) $attachment_id,
+ 'src' => current( $attachment ),
+ 'thumbnail' => current( $thumbnail ),
+ 'srcset' => (string) wp_get_attachment_image_srcset( $attachment_id, 'full' ),
+ 'sizes' => (string) wp_get_attachment_image_sizes( $attachment_id, 'full' ),
+ 'thumbnail_srcset' => (string) wp_get_attachment_image_srcset( $attachment_id, 'woocommerce_thumbnail' ),
+ 'thumbnail_sizes' => (string) wp_get_attachment_image_sizes( $attachment_id, 'woocommerce_thumbnail' ),
+ 'name' => get_the_title( $attachment_id ),
+ 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
];
}