diff --git a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php
index c55129d761..bd6bb88800 100644
--- a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php
+++ b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php
@@ -146,21 +146,44 @@ private function process_img( OD_HTML_Tag_Processor $processor, OD_Tag_Visitor_C
$processor->remove_attribute( 'fetchpriority' );
}
- // Ensure that sizes=auto is set properly.
- $sizes = $processor->get_attribute( 'sizes' );
- if ( is_string( $sizes ) ) {
+ // Ensure that sizes is set properly when it is a responsive image (it has a srcset attribute).
+ if ( is_string( $processor->get_attribute( 'srcset' ) ) ) {
+ $sizes = $processor->get_attribute( 'sizes' );
+ if ( ! is_string( $sizes ) ) {
+ $sizes = '';
+ }
+
$is_lazy = 'lazy' === $this->get_attribute_value( $processor, 'loading' );
$has_auto = $this->sizes_attribute_includes_valid_auto( $sizes );
if ( $is_lazy && ! $has_auto ) {
- $processor->set_attribute( 'sizes', "auto, $sizes" );
+ $new_sizes = 'auto';
+ if ( '' !== trim( $sizes, " \t\f\r\n" ) ) {
+ $new_sizes .= ', ';
+ }
+ $sizes = $new_sizes . $sizes;
} elseif ( ! $is_lazy && $has_auto ) {
// Remove auto from the beginning of the list.
- $processor->set_attribute(
- 'sizes',
- (string) preg_replace( '/^[ \t\f\r\n]*auto[ \t\f\r\n]*(,[ \t\f\r\n]*)?/i', '', $sizes )
- );
+ $sizes = (string) preg_replace( '/^[ \t\f\r\n]*auto[ \t\f\r\n]*(,[ \t\f\r\n]*)?/i', '', $sizes );
+ }
+
+ // Compute more accurate sizes when it isn't lazy-loaded and sizes=auto isn't taking care of it.
+ if ( ! $is_lazy ) {
+ $computed_sizes = $this->compute_sizes( $context );
+ if ( count( $computed_sizes ) > 0 ) {
+ $new_sizes = join( ', ', $computed_sizes );
+
+ // Preserve the original sizes as a fallback when URL Metrics are missing from one or more viewport group.
+ // Note that when all groups are populated, the media features will span all possible viewport widths from
+ // zero to infinity, so there is no need to include the original sizes since they will never match.
+ if ( '' !== $sizes && ! $context->url_metric_group_collection->is_every_group_populated() ) {
+ $new_sizes .= ", $sizes";
+ }
+ $sizes = $new_sizes;
+ }
}
+
+ $processor->set_attribute( 'sizes', $sizes );
}
$parent_tag = $this->get_parent_tag_name( $context );
@@ -385,4 +408,38 @@ private function sizes_attribute_includes_valid_auto( string $sizes_attr ): bool
return 'auto' === $sizes_attr || str_starts_with( $sizes_attr, 'auto,' );
}
}
+
+ /**
+ * Computes responsive sizes for the current element based on its boundingClientRect width captured in URL Metrics.
+ *
+ * @since n.e.x.t
+ *
+ * @param OD_Tag_Visitor_Context $context Context.
+ * @return non-empty-string[] Computed sizes.
+ */
+ private function compute_sizes( OD_Tag_Visitor_Context $context ): array {
+ $sizes = array();
+
+ $xpath = $context->processor->get_xpath();
+ foreach ( $context->url_metric_group_collection as $group ) {
+ // Obtain the maximum width that the image appears among all URL Metrics collected for this viewport group.
+ $element_max_width = 0;
+ foreach ( $group->get_xpath_elements_map()[ $xpath ] ?? array() as $element ) {
+ $element_max_width = max( $element_max_width, $element->get_bounding_client_rect()['width'] );
+ }
+
+ // Use the maximum width as the size for image in this breakpoint.
+ if ( $element_max_width > 0 ) {
+ $size = sprintf( '%dpx', $element_max_width );
+ $media_feature = od_generate_media_query( $group->get_minimum_viewport_width(), $group->get_maximum_viewport_width() );
+ if ( null !== $media_feature ) {
+ // Note: The null case only happens when a site has filtered od_breakpoint_max_widths to be an empty array, meaning there is only one viewport group.
+ $size = "$media_feature $size";
+ }
+ $sizes[] = $size;
+ }
+ }
+
+ return $sizes;
+ }
}
diff --git a/plugins/image-prioritizer/readme.txt b/plugins/image-prioritizer/readme.txt
index c1e47602c3..e7ca146c2f 100644
--- a/plugins/image-prioritizer/readme.txt
+++ b/plugins/image-prioritizer/readme.txt
@@ -7,7 +7,7 @@ License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Tags: performance, optimization, image, lcp, lazy-load
-Prioritizes the loading of images and videos based on how visible they are to actual visitors; adds fetchpriority and applies lazy loading.
+Prioritizes the loading of images and videos based on how they appear to actual visitors: adds fetchpriority, preloads, lazy-loads, and sets sizes.
== Description ==
@@ -27,7 +27,9 @@ The current optimizations include:
1. Apply lazy loading to `IMG` tags based on whether they appear in any breakpoint’s initial viewport.
2. Implement lazy loading of CSS background images added via inline `style` attributes.
3. Lazy-load `VIDEO` tags by setting the appropriate attributes based on whether they appear in the initial viewport. If a `VIDEO` is the LCP element, it gets `preload=auto`; if it is in an initial viewport, the `preload=metadata` default is left; if it is not in an initial viewport, it gets `preload=none`. Lazy-loaded videos also get initial `preload`, `autoplay`, and `poster` attributes restored when the `VIDEO` is going to enter the viewport.
-5. Ensure that [`sizes=auto`](https://make.wordpress.org/core/2024/10/18/auto-sizes-for-lazy-loaded-images-in-wordpress-6-7/) is added to all lazy-loaded `IMG` elements.
+5. Responsive image sizes:
+ 1. Compute the `sizes` attribute using the widths of an image collected from URL Metrics for each breakpoint (when not lazy-loaded since then handled by `sizes=auto`).
+ 2. Ensure [`sizes=auto`](https://make.wordpress.org/core/2024/10/18/auto-sizes-for-lazy-loaded-images-in-wordpress-6-7/) is set on `IMG` tags after setting correct lazy-loading (above).
6. Reduce the size of the `poster` image of a `VIDEO` from full size to the size appropriate for the maximum width of the video (on desktop).
**This plugin requires the [Optimization Detective](https://wordpress.org/plugins/optimization-detective/) plugin as a dependency.** Please refer to that plugin for additional background on how this plugin works as well as additional developer options.
diff --git a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data/set-up.php b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data/set-up.php
index 5839f6eb3c..b0acbfcb07 100644
--- a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data/set-up.php
+++ b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data/set-up.php
@@ -10,13 +10,6 @@ static function () use ( $breakpoint_max_widths ) {
}
);
- $outside_viewport_rect = array_merge(
- $test_case->get_sample_dom_rect(),
- array(
- 'top' => 100000,
- )
- );
-
foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) {
for ( $i = 0; $i < $sample_size; $i++ ) {
OD_URL_Metrics_Post_Type::store_url_metric(
@@ -29,8 +22,7 @@ static function () use ( $breakpoint_max_widths ) {
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[2][self::DIV]',
'isLCP' => false,
'intersectionRatio' => 0.0,
- 'intersectionRect' => $outside_viewport_rect,
- 'boundingClientRect' => $outside_viewport_rect,
+ 'boundingClientRect' => array( 'top' => 100000 ),
),
),
)
diff --git a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/set-up.php b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/set-up.php
index dc4dc10f49..602fc0221d 100644
--- a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/set-up.php
+++ b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/set-up.php
@@ -9,13 +9,6 @@ static function () use ( $breakpoint_max_widths ) {
}
);
- $outside_viewport_rect = array_merge(
- $test_case->get_sample_dom_rect(),
- array(
- 'top' => 100000,
- )
- );
-
foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) {
OD_URL_Metrics_Post_Type::store_url_metric(
od_get_url_metrics_slug( od_get_normalized_query_vars() ),
@@ -27,8 +20,7 @@ static function () use ( $breakpoint_max_widths ) {
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[2][self::DIV]',
'isLCP' => false,
'intersectionRatio' => 0.0,
- 'intersectionRect' => $outside_viewport_rect,
- 'boundingClientRect' => $outside_viewport_rect,
+ 'boundingClientRect' => array( 'top' => 100000 ),
),
),
)
diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data/set-up.php b/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data/set-up.php
index fe81ed23db..bdf18f9142 100644
--- a/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data/set-up.php
+++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-background-image-and-lazy-loaded-background-image-outside-viewport-with-fully-populated-sample-data/set-up.php
@@ -1,12 +1,5 @@
get_sample_dom_rect(),
- array(
- 'top' => 100000,
- )
- );
-
$test_case->populate_url_metrics(
array(
array(
@@ -17,15 +10,13 @@
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[3][self::DIV]',
'isLCP' => false,
'intersectionRatio' => 0.0,
- 'intersectionRect' => $outside_viewport_rect,
- 'boundingClientRect' => $outside_viewport_rect,
+ 'boundingClientRect' => array( 'top' => 100000 ),
),
array(
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[4][self::DIV]',
'isLCP' => false,
'intersectionRatio' => 0.0,
- 'intersectionRect' => $outside_viewport_rect,
- 'boundingClientRect' => $outside_viewport_rect,
+ 'boundingClientRect' => array( 'top' => 100000 ),
),
)
);
diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/buffer.html b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/buffer.html
index 06087ee6b6..806fd84b35 100644
--- a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/buffer.html
+++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/buffer.html
@@ -11,9 +11,9 @@
Pretend this is a super long paragraph that pushes the next image mostly out of the initial viewport.
-
+
Now the following image is definitely outside the initial viewport.
This post does have a featured image, and the server-side heuristics in WordPress cause it to get fetchpriority=high, but it should not have this since it is out of the viewport on mobile.