Skip to content

Commit

Permalink
Merge pull request #1840 from WordPress/try/ip-sizes-optimization
Browse files Browse the repository at this point in the history
Compute responsive `sizes` attribute based on the `width` from the `boundingClientRect` in captured URL Metrics
  • Loading branch information
westonruter authored Feb 8, 2025
2 parents e845dd8 + 4d0251a commit 6459571
Show file tree
Hide file tree
Showing 16 changed files with 165 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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;
}
}
6 changes: 4 additions & 2 deletions plugins/image-prioritizer/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ==

Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 ),
),
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() ),
Expand All @@ -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 ),
),
),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
<?php
return static function ( Test_Image_Prioritizer_Helper $test_case ): void {
$outside_viewport_rect = array_merge(
$test_case->get_sample_dom_rect(),
array(
'top' => 100000,
)
);

$test_case->populate_url_metrics(
array(
array(
Expand All @@ -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 ),
),
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
<img src="https://example.com/foo3.jpg" alt="Foo" width="1200" height="800" fetchpriority="high" srcset="https://example.com/foo3-480w.jpg 480w, https://example.com/foo3-800w.jpg 800w" sizes="(max-width: 600px) 480px, 800px">
</div>
<p>Pretend this is a super long paragraph that pushes the next image mostly out of the initial viewport.</p>
<img src="https://example.com/bar.jpg" alt="Bar" width="10" height="10" fetchpriority="high" loading="lazy">
<img src="https://example.com/bar.jpg" alt="Bar" width="1024" height="768" fetchpriority="high" loading="lazy" srcset="https://example.com/bar-300w.jpg 300w, https://example.com/bar-480w.jpg 480w, https://example.com/bar-800w.jpg 800w, https://example.com/bar-900w.jpg 900w, https://example.com/bar-1000w.jpg 1000w" sizes="(max-width: 1024px) 100vw, 1024px">
<p>Now the following image is definitely outside the initial viewport.</p>
<img src="https://example.com/baz.jpg" alt="Baz" width="10" height="10" fetchpriority="high">
<img src="https://example.com/baz.jpg" alt="Baz" width="3000" height="1500" fetchpriority="high" srcset="https://example.com/baz-300w.jpg 300w, https://example.com/baz-480w.jpg 480w, https://example.com/baz-800w.jpg 800w, https://example.com/baz-900w.jpg 900w, https://example.com/baz-1000w.jpg 1000w, https://example.com/baz-1500w.jpg 1500w, https://example.com/baz-2000w.jpg 2000w, https://example.com/baz-2500w.jpg 2500w" sizes="(max-width: 3000px) 100vw, 3000px">
<img src="https://example.com/qux.jpg" alt="Qux" width="10" height="10" fetchpriority="high" loading="eager">
<img src="https://example.com/quux.jpg" alt="Quux" width="10" height="10" loading="LAZY"><!-- This one is all good. -->
</div>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
<?php
return static function ( Test_Image_Prioritizer_Helper $test_case ): void {
$slug = od_get_url_metrics_slug( od_get_normalized_query_vars() );
$sample_size = od_get_url_metrics_breakpoint_sample_size();
$outside_viewport_rect = array_merge(
$test_case->get_sample_dom_rect(),
array(
'top' => 100000,
)
);
$slug = od_get_url_metrics_slug( od_get_normalized_query_vars() );
$sample_size = od_get_url_metrics_breakpoint_sample_size();
foreach ( array_merge( od_get_breakpoint_max_widths(), array( 1000 ) ) as $viewport_width ) {
for ( $i = 0; $i < $sample_size; $i++ ) {
OD_URL_Metrics_Post_Type::store_url_metric(
Expand All @@ -31,31 +25,31 @@
'intersectionRatio' => 0.0, // Subsequent carousel slide.
),
array(
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[3][self::IMG]',
'isLCP' => false,
'intersectionRatio' => 0 === $i ? 0.5 : 0.0, // Make sure that the _max_ intersection ratio is considered.
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[3][self::IMG]',
'isLCP' => false,
'intersectionRatio' => 0 === $i ? 0.5 : 0.0, // Make sure that the _max_ intersection ratio is considered.
'boundingClientRect' => array(
'width' => $viewport_width - 10,
),
),
// All are outside all initial viewports.
array(
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[5][self::IMG]',
'isLCP' => false,
'intersectionRatio' => 0.0,
'intersectionRect' => $outside_viewport_rect,
'boundingClientRect' => $outside_viewport_rect,
'boundingClientRect' => array( 'top' => 100000 ),
),
array(
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[6][self::IMG]',
'isLCP' => false,
'intersectionRatio' => 0.0,
'intersectionRect' => $outside_viewport_rect,
'boundingClientRect' => $outside_viewport_rect,
'boundingClientRect' => array( 'top' => 100000 ),
),
array(
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[7][self::IMG]',
'isLCP' => false,
'intersectionRatio' => 0.0,
'intersectionRect' => $outside_viewport_rect,
'boundingClientRect' => $outside_viewport_rect,
'boundingClientRect' => array( 'top' => 100000 ),
),
),
)
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 6459571

Please sign in to comment.