Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto Sizes for Lazy-loaded Images #7253

Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/wp-includes/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,15 @@ function wp_get_attachment_image( $attachment_id, $size = 'thumbnail', $icon = f
}
}

// Adds auto to the sizes attribute if applicable.
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
if (
( isset( $attr['loading'] ) && 'lazy' === $attr['loading'] ) &&
isset( $attr['sizes'] ) &&
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
! wp_sizes_attribute_includes_valid_auto( $attr['sizes'] )
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
) {
$attr['sizes'] = 'auto, ' . $attr['sizes'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this remove auto if it is present in $attr['sizes'] while $attr['loading'] is not lazy? This is implemented in WordPress/performance#1476.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this remove auto if it is present in $attr['sizes'] while $attr['loading'] is not lazy? This is implemented in WordPress/performance#1476.

Do any of the implementations respect it in spite of the spec if the CSS is available and the image isn't set for lazy loading? If so then I think it would be good to keep it given the sizes attributes can be inaccurate in many situations and the CSS may be in the HTML header and therefore available.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this remove auto if it is present in $attr['sizes'] while $attr['loading'] is not lazy? This is implemented in WordPress/performance#1476.

@westonruter Core only adds the "auto" in sizes when image is lazy-loaded so if user added that through filter then core will not remove "auto".

@peterwilsoncc Even if the CSS is available early in the document (such as in the HTML header), implementations do not dynamically adjust the sizes attribute based on CSS unless lazy loading is enabled.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed as Core doesn't add sizes="auto" in a way where this could be relevant. If a plugin does that, it's doing it wrong.

Now I'm not strongly against adding this, but it's not a requirement for this feature. We could discuss whether this is worth adding in a separate ticket, or wait if it comes up as a real use-case problem somewhere.

}

/**
* Filters the list of attachment image attributes.
*
Expand Down Expand Up @@ -1917,6 +1926,9 @@ function wp_filter_content_tags( $content, $context = null ) {
// Add loading optimization attributes if applicable.
$filtered_image = wp_img_tag_add_loading_optimization_attrs( $filtered_image, $context );

// Adds auto to the sizes attribute if applicable.
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
$filtered_image = wp_img_tag_add_auto_sizes( $filtered_image );

/**
* Filters an img tag within the content for a given context.
*
Expand Down Expand Up @@ -1963,6 +1975,59 @@ function wp_filter_content_tags( $content, $context = null ) {
return $content;
}

/**
* Adds auto to the sizes attribute to the image, if image lazy loaded.
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
*
* @since 6.7.0
*
* @param string $html The HTML image tag markup being filtered.
* @return string The filtered HTML image tag markup.
*/
function wp_img_tag_add_auto_sizes( $image ) {
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
$processor = new WP_HTML_Tag_Processor( $image );

// Bail if there is no IMG tag.
if ( ! $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
return $image;
}

// Bail early if the image is not lazy-loaded.
$value = $processor->get_attribute( 'loading' );
if ( ! is_string( $value ) || 'lazy' !== strtolower( trim( $value, " \t\f\r\n" ) ) ) {
return $image;
}

$sizes = $processor->get_attribute( 'sizes' );

// Bail early if the image is not responsive.
if ( ! is_string( $sizes ) ) {
return $image;
}

// Don't add 'auto' to the sizes attribute if it already exists.
if ( wp_sizes_attribute_includes_valid_auto( $sizes ) ) {
return $image;
}

$processor->set_attribute( 'sizes', "auto, $sizes" );
return $processor->get_updated_html();
}

/**
* Checks whether the given 'sizes' attribute includes the 'auto' keyword as the first item in the list.
*
* Per the HTML spec, if present it must be the first entry.
*
* @since 6.7.0
*
* @param string $sizes_attr The 'sizes' attribute value.
* @return bool True if the 'auto' keyword is present, false otherwise.
*/
function wp_sizes_attribute_includes_valid_auto( string $sizes_attr ) {
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
$token = strtok( strtolower( $sizes_attr ), ',' );
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
return false !== $token && 'auto' === trim( $token, " \t\f\r\n" );
}

/**
* Adds optimization attributes to an `img` HTML tag.
*
Expand Down
279 changes: 278 additions & 1 deletion tests/phpunit/tests/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -2467,6 +2467,9 @@ public function test_wp_calculate_image_srcset_animated_gifs() {
* @requires function imagejpeg
*/
public function test_wp_filter_content_tags_schemes() {
// Disable lazy loading attribute to not add the 'auto' keyword to the `sizes` attribute.
add_filter( 'wp_img_tag_add_loading_attr', '__return_false' );

$image_meta = wp_get_attachment_metadata( self::$large_id );
$size_array = $this->get_image_size_array_from_meta( $image_meta, 'medium' );

Expand Down Expand Up @@ -2680,7 +2683,7 @@ public function test_wp_get_attachment_image_should_use_wp_get_attachment_metada
'src="' . $uploads_url . 'test-image-testsize-999x999.jpg" ' .
'class="attachment-testsize size-testsize" alt="" decoding="async" loading="lazy" ' .
'srcset="' . $uploads_url . 'test-image-testsize-999x999.jpg 999w, ' . $uploads_url . $basename . '-150x150.jpg 150w" ' .
'sizes="(max-width: 999px) 100vw, 999px" />';
'sizes="auto, (max-width: 999px) 100vw, 999px" />';

$actual = wp_get_attachment_image( self::$large_id, 'testsize' );

Expand Down Expand Up @@ -5117,6 +5120,9 @@ static function ( $loading_attrs ) {
}
);

// Do not calculate sizes attribute as it is irrelevant for this test.
add_filter( 'wp_calculate_image_sizes', '__return_false' );

// Add shortcode that prints a large image, and a block type that wraps it.
add_shortcode(
'full_image',
Expand Down Expand Up @@ -5992,6 +5998,277 @@ static function ( $loading_attrs ) {
);
}

/**
* Test generated markup for an image with lazy loading gets auto-sizes.
*
* @ticket 61847
*/
public function test_image_with_lazy_loading_has_auto_sizes() {
$this->assertStringContainsString(
'sizes="auto, ',
wp_get_attachment_image( self::$large_id, 'large', false, array( 'loading' => 'lazy' ) ),
'Failed asserting that the sizes attribute for a lazy-loaded image includes "auto".'
);
}

/**
* Test generated markup for an image without lazy loading does not get auto-sizes.
*
* @ticket 61847
*/
public function test_image_without_lazy_loading_does_not_have_auto_sizes() {
$this->assertStringNotContainsString(
'sizes="auto, ',
wp_get_attachment_image( self::$large_id, 'large', false, array( 'loading' => false ) ),
'Failed asserting that the sizes attribute for an image without lazy loading does not include "auto".'
);
}

/**
* Test content filtered markup with lazy loading gets auto-sizes.
*
* @ticket 61847
*
* @covers ::wp_img_tag_add_auto_sizes
*/
public function test_content_image_with_lazy_loading_has_auto_sizes() {
// Force lazy loading attribute.
add_filter( 'wp_img_tag_add_loading_attr', '__return_true' );

$this->assertStringContainsString(
'sizes="auto, (max-width: 1024px) 100vw, 1024px"',
wp_filter_content_tags( get_image_tag( self::$large_id, '', '', '', 'large' ) ),
'Failed asserting that the sizes attribute for a content image with lazy loading includes "auto" with the expected sizes.'
);
}

/**
* Test content filtered markup without lazy loading does not get auto-sizes.
*
* @ticket 61847
*
* @covers ::wp_img_tag_add_auto_sizes
*/
public function test_content_image_without_lazy_loading_does_not_have_auto_sizes() {
// Disable lazy loading attribute.
add_filter( 'wp_img_tag_add_loading_attr', '__return_false' );

$this->assertStringContainsString(
'sizes="(max-width: 1024px) 100vw, 1024px"',
wp_filter_content_tags( get_image_tag( self::$large_id, '', '', '', 'large' ) ),
'Failed asserting that the sizes attribute for a content image without lazy loading does not include "auto" with the expected sizes.'
);
}

/**
* Test generated markup for an image with 'auto' keyword already present in sizes does not receive it again.
*
* @ticket 61847
*
* @covers ::wp_img_tag_add_auto_sizes
* @covers ::wp_sizes_attribute_includes_valid_auto
*
* @dataProvider data_image_with_existing_auto_sizes
*
* @param string $initial_sizes The initial sizes attribute to test.
* @param bool $expected_processed Whether the auto sizes should be processed or not.
*/
public function test_image_with_existing_auto_sizes_is_not_processed_again( string $initial_sizes, bool $expected_processed ) {
$image_tag = wp_get_attachment_image(
self::$large_id,
'large',
false,
array(
// Force pre-existing 'sizes' attribute and lazy-loading.
'sizes' => $initial_sizes,
'loading' => 'lazy',
)
);
if ( $expected_processed ) {
$this->assertStringContainsString(
'sizes="auto, ' . $initial_sizes . '"',
$image_tag,
'Failed asserting that "auto" keyword is not added to sizes attribute when it already exists.'
);
} else {
$this->assertStringContainsString(
'sizes="' . $initial_sizes . '"',
$image_tag,
'Failed asserting that "auto" keyword is not added to sizes attribute when it already exists.'
);
}
}

/**
* Test content filtered markup with 'auto' keyword already present in sizes does not receive it again.
*
* @ticket 61847
*
* @covers ::wp_img_tag_add_auto_sizes
* @covers ::wp_sizes_attribute_includes_valid_auto
*
* @dataProvider data_image_with_existing_auto_sizes
*
* @param string $initial_sizes The initial sizes attribute to test.
* @param bool $expected_processed Whether the auto sizes should be processed or not.
*/
public function test_content_image_with_existing_auto_sizes_is_not_processed_again( string $initial_sizes, bool $expected_processed ) {
// Force lazy loading attribute.
add_filter( 'wp_img_tag_add_loading_attr', '__return_true' );

add_filter(
'get_image_tag',
static function ( $html ) use ( $initial_sizes ) {
return str_replace(
'" />',
'" sizes="' . $initial_sizes . '" />',
$html
);
}
);

$image_content = wp_filter_content_tags( get_image_tag( self::$large_id, '', '', '', 'large' ) );
if ( $expected_processed ) {
$this->assertStringContainsString(
'sizes="auto, ' . $initial_sizes . '"',
$image_content,
'Failed asserting that "auto" keyword is not added to sizes attribute in filtered content when it already exists.'
);
} else {
$this->assertStringContainsString(
'sizes="' . $initial_sizes . '"',
$image_content,
'Failed asserting that "auto" keyword is not added to sizes attribute in filtered content when it already exists.'
);
}
}

/**
* Returns data for the above test methods to assert correct behavior with a pre-existing sizes attribute.
*
* @return array<string, mixed[]> Arguments for the test scenarios.
*/
public function data_image_with_existing_auto_sizes() {
return array(
'not present' => array(
'(max-width: 1024px) 100vw, 1024px',
true,
),
'in beginning, without space' => array(
'auto,(max-width: 1024px) 100vw, 1024px',
false,
),
'in beginning, with space' => array(
'auto, (max-width: 1024px) 100vw, 1024px',
false,
),
'sole keyword' => array(
'auto',
false,
),
'with space before' => array(
' auto, (max-width: 1024px) 100vw, 1024px',
false,
),
'with uppercase' => array(
'AUTO, (max-width: 1024px) 100vw, 1024px',
false,
),

/*
* The following scenarios technically include the 'auto' keyword,
* but it is in the wrong place, as per the HTML spec it must be
* the first entry in the list.
* Therefore in these invalid cases the 'auto' keyword should still
* be added to the beginning of the list.
*/
'within, without space' => array(
'(max-width: 1024px) 100vw, auto,1024px',
true,
),
'within, with space' => array(
'(max-width: 1024px) 100vw, auto, 1024px',
true,
),
'at the end, without space' => array(
'(max-width: 1024px) 100vw,auto',
true,
),
'at the end, with space' => array(
'(max-width: 1024px) 100vw, auto',
true,
),
);
}

/**
* Data provider for test_wp_img_tag_add_auto_sizes().
*
* @return array<string, mixed>
*/
public function data_provider_to_test_wp_img_tag_add_auto_sizes() {
return array(
'expected_with_single_quoted_attributes' => array(
'input' => "<img src='https://example.com/foo-300x225.jpg' srcset='https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w' sizes='(max-width: 650px) 100vw, 650px' loading='lazy'>",
'expected' => "<img src='https://example.com/foo-300x225.jpg' srcset='https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w' sizes=\"auto, (max-width: 650px) 100vw, 650px\" loading='lazy'>",
),
'expected_with_data_sizes_attribute' => array(
'input' => '<img data-tshirt-sizes="S M L" src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" loading="lazy">',
'expected' => '<img data-tshirt-sizes="S M L" src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="auto, (max-width: 650px) 100vw, 650px" loading="lazy">',
),
'expected_with_data_sizes_attribute_already_present' => array(
'input' => '<img data-tshirt-sizes="S M L" src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="AUTO, (max-width: 650px) 100vw, 650px" loading="lazy">',
'expected' => '<img data-tshirt-sizes="S M L" src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="AUTO, (max-width: 650px) 100vw, 650px" loading="lazy">',
),
'not_expected_with_loading_lazy_in_attr_value' => array(
'input' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" alt=\'This is the LCP image and it should not get loading="lazy"!\'>',
'expected' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" alt=\'This is the LCP image and it should not get loading="lazy"!\'>',
),
'not_expected_with_data_loading_attribute_present' => array(
'input' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" data-removed-loading="lazy">',
'expected' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" data-removed-loading="lazy">',
),
'expected_when_attributes_have_spaces_after_them' => array(
'input' => '<img src = "https://example.com/foo-300x225.jpg" srcset = "https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes = "(max-width: 650px) 100vw, 650px" loading = "lazy">',
'expected' => '<img src = "https://example.com/foo-300x225.jpg" srcset = "https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="auto, (max-width: 650px) 100vw, 650px" loading = "lazy">',
),
'expected_when_attributes_are_upper_case' => array(
'input' => '<IMG SRC="https://example.com/foo-300x225.jpg" SRCSET="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" SIZES="(max-width: 650px) 100vw, 650px" LOADING="LAZY">',
'expected' => '<IMG SRC="https://example.com/foo-300x225.jpg" SRCSET="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="auto, (max-width: 650px) 100vw, 650px" LOADING="LAZY">',
),
'expected_when_loading_lazy_lacks_quotes' => array(
'input' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" loading=lazy>',
'expected' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="auto, (max-width: 650px) 100vw, 650px" loading=lazy>',
),
'expected_when_loading_lazy_has_whitespace' => array(
'input' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 650px) 100vw, 650px" loading=" lazy ">',
'expected' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="auto, (max-width: 650px) 100vw, 650px" loading=" lazy ">',
),
'not_expected_when_sizes_auto_lacks_quotes' => array(
'input' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes=auto loading="lazy">',
'expected' => '<img src="https://example.com/foo-300x225.jpg" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes=auto loading="lazy">',
),
);
}

/**
* @ticket 61847
*
* @covers ::wp_img_tag_add_auto_sizes
*
* @dataProvider data_provider_to_test_wp_img_tag_add_auto_sizes
*
* @param string $input The input HTML string.
* @param string $expected The expected output HTML string.
*/
public function test_wp_img_tag_add_auto_sizes( string $input, string $expected ) {
$this->assertSame(
$expected,
wp_img_tag_add_auto_sizes( $input ),
'Failed asserting that "auto" keyword is correctly added or not added to sizes attribute in the image tag.'
);
}

/**
* Helper method to keep track of the last context returned by the 'wp_get_attachment_image_context' filter.
*
Expand Down
Loading