Skip to content

Commit 66b31f2

Browse files
authored
Merge branch 'trunk' into add/code_coverage_speculation_rules
2 parents 8fd7597 + d21b74d commit 66b31f2

File tree

50 files changed

+1054
-321
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1054
-321
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"szepeviktor/phpstan-wordpress": "^1.3",
3131
"wp-coding-standards/wpcs": "^3.1",
3232
"wp-phpunit/wp-phpunit": "^6.5",
33-
"yoast/phpunit-polyfills": "^3.1",
33+
"yoast/phpunit-polyfills": "^4.0",
3434
"phpstan/php-8-stubs": "^0.4.0",
3535
"phpstan/phpstan-strict-rules": "^1.6"
3636
},

composer.lock

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,21 +146,44 @@ private function process_img( OD_HTML_Tag_Processor $processor, OD_Tag_Visitor_C
146146
$processor->remove_attribute( 'fetchpriority' );
147147
}
148148

149-
// Ensure that sizes=auto is set properly.
150-
$sizes = $processor->get_attribute( 'sizes' );
151-
if ( is_string( $sizes ) ) {
149+
// Ensure that sizes is set properly when it is a responsive image (it has a srcset attribute).
150+
if ( is_string( $processor->get_attribute( 'srcset' ) ) ) {
151+
$sizes = $processor->get_attribute( 'sizes' );
152+
if ( ! is_string( $sizes ) ) {
153+
$sizes = '';
154+
}
155+
152156
$is_lazy = 'lazy' === $this->get_attribute_value( $processor, 'loading' );
153157
$has_auto = $this->sizes_attribute_includes_valid_auto( $sizes );
154158

155159
if ( $is_lazy && ! $has_auto ) {
156-
$processor->set_attribute( 'sizes', "auto, $sizes" );
160+
$new_sizes = 'auto';
161+
if ( '' !== trim( $sizes, " \t\f\r\n" ) ) {
162+
$new_sizes .= ', ';
163+
}
164+
$sizes = $new_sizes . $sizes;
157165
} elseif ( ! $is_lazy && $has_auto ) {
158166
// Remove auto from the beginning of the list.
159-
$processor->set_attribute(
160-
'sizes',
161-
(string) preg_replace( '/^[ \t\f\r\n]*auto[ \t\f\r\n]*(,[ \t\f\r\n]*)?/i', '', $sizes )
162-
);
167+
$sizes = (string) preg_replace( '/^[ \t\f\r\n]*auto[ \t\f\r\n]*(,[ \t\f\r\n]*)?/i', '', $sizes );
168+
}
169+
170+
// Compute more accurate sizes when it isn't lazy-loaded and sizes=auto isn't taking care of it.
171+
if ( ! $is_lazy ) {
172+
$computed_sizes = $this->compute_sizes( $context );
173+
if ( count( $computed_sizes ) > 0 ) {
174+
$new_sizes = join( ', ', $computed_sizes );
175+
176+
// Preserve the original sizes as a fallback when URL Metrics are missing from one or more viewport group.
177+
// Note that when all groups are populated, the media features will span all possible viewport widths from
178+
// zero to infinity, so there is no need to include the original sizes since they will never match.
179+
if ( '' !== $sizes && ! $context->url_metric_group_collection->is_every_group_populated() ) {
180+
$new_sizes .= ", $sizes";
181+
}
182+
$sizes = $new_sizes;
183+
}
163184
}
185+
186+
$processor->set_attribute( 'sizes', $sizes );
164187
}
165188

166189
$parent_tag = $this->get_parent_tag_name( $context );
@@ -385,4 +408,38 @@ private function sizes_attribute_includes_valid_auto( string $sizes_attr ): bool
385408
return 'auto' === $sizes_attr || str_starts_with( $sizes_attr, 'auto,' );
386409
}
387410
}
411+
412+
/**
413+
* Computes responsive sizes for the current element based on its boundingClientRect width captured in URL Metrics.
414+
*
415+
* @since n.e.x.t
416+
*
417+
* @param OD_Tag_Visitor_Context $context Context.
418+
* @return non-empty-string[] Computed sizes.
419+
*/
420+
private function compute_sizes( OD_Tag_Visitor_Context $context ): array {
421+
$sizes = array();
422+
423+
$xpath = $context->processor->get_xpath();
424+
foreach ( $context->url_metric_group_collection as $group ) {
425+
// Obtain the maximum width that the image appears among all URL Metrics collected for this viewport group.
426+
$element_max_width = 0;
427+
foreach ( $group->get_xpath_elements_map()[ $xpath ] ?? array() as $element ) {
428+
$element_max_width = max( $element_max_width, $element->get_bounding_client_rect()['width'] );
429+
}
430+
431+
// Use the maximum width as the size for image in this breakpoint.
432+
if ( $element_max_width > 0 ) {
433+
$size = sprintf( '%dpx', $element_max_width );
434+
$media_feature = od_generate_media_query( $group->get_minimum_viewport_width(), $group->get_maximum_viewport_width() );
435+
if ( null !== $media_feature ) {
436+
// 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.
437+
$size = "$media_feature $size";
438+
}
439+
$sizes[] = $size;
440+
}
441+
}
442+
443+
return $sizes;
444+
}
388445
}

plugins/image-prioritizer/readme.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ License: GPLv2 or later
77
License URI: https://www.gnu.org/licenses/gpl-2.0.html
88
Tags: performance, optimization, image, lcp, lazy-load
99

10-
Prioritizes the loading of images and videos based on how visible they are to actual visitors; adds fetchpriority and applies lazy loading.
10+
Prioritizes the loading of images and videos based on how they appear to actual visitors: adds fetchpriority, preloads, lazy-loads, and sets sizes.
1111

1212
== Description ==
1313

1414
This plugin optimizes the loading of images (and videos) with prioritization to improve [Largest Contentful Paint](https://web.dev/articles/lcp) (LCP), lazy loading, and more accurate image size selection.
1515

1616
The current optimizations include:
1717

18-
1. Add breakpoint-specific `fetchpriority=high` preload links (`LINK[rel=preload]`) for image URLs of LCP elements:
18+
1. Add breakpoint-specific `fetchpriority=high` preload links (both as `LINK[rel=preload]` elements and `Link` response headers) for image URLs of LCP elements:
1919
1. An `IMG` element, including the `srcset`/`sizes` attributes supplied as `imagesrcset`/`imagesizes` on the `LINK`.
2020
2. The first `SOURCE` element with a `type` attribute in a `PICTURE` element. (Art-directed `PICTURE` elements using media queries are not supported.)
2121
3. An element with a CSS `background-image` inline `style` attribute.
@@ -27,7 +27,9 @@ The current optimizations include:
2727
1. Apply lazy loading to `IMG` tags based on whether they appear in any breakpoint’s initial viewport.
2828
2. Implement lazy loading of CSS background images added via inline `style` attributes.
2929
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.
30-
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.
30+
5. Responsive image sizes:
31+
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`).
32+
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).
3133
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).
3234

3335
**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.

plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-on-all-breakpoints-but-not-desktop-with-fully-populated-sample-data/set-up.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,6 @@ static function () use ( $breakpoint_max_widths ) {
1010
}
1111
);
1212

13-
$outside_viewport_rect = array_merge(
14-
$test_case->get_sample_dom_rect(),
15-
array(
16-
'top' => 100000,
17-
)
18-
);
19-
2013
foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) {
2114
for ( $i = 0; $i < $sample_size; $i++ ) {
2215
OD_URL_Metrics_Post_Type::store_url_metric(
@@ -29,8 +22,7 @@ static function () use ( $breakpoint_max_widths ) {
2922
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[2][self::DIV]',
3023
'isLCP' => false,
3124
'intersectionRatio' => 0.0,
32-
'intersectionRect' => $outside_viewport_rect,
33-
'boundingClientRect' => $outside_viewport_rect,
25+
'boundingClientRect' => array( 'top' => 100000 ),
3426
),
3527
),
3628
)

plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/set-up.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,6 @@ static function () use ( $breakpoint_max_widths ) {
99
}
1010
);
1111

12-
$outside_viewport_rect = array_merge(
13-
$test_case->get_sample_dom_rect(),
14-
array(
15-
'top' => 100000,
16-
)
17-
);
18-
1912
foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) {
2013
OD_URL_Metrics_Post_Type::store_url_metric(
2114
od_get_url_metrics_slug( od_get_normalized_query_vars() ),
@@ -27,8 +20,7 @@ static function () use ( $breakpoint_max_widths ) {
2720
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[2][self::DIV]',
2821
'isLCP' => false,
2922
'intersectionRatio' => 0.0,
30-
'intersectionRect' => $outside_viewport_rect,
31-
'boundingClientRect' => $outside_viewport_rect,
23+
'boundingClientRect' => array( 'top' => 100000 ),
3224
),
3325
),
3426
)

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

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
<?php
22
return static function ( Test_Image_Prioritizer_Helper $test_case ): void {
3-
$outside_viewport_rect = array_merge(
4-
$test_case->get_sample_dom_rect(),
5-
array(
6-
'top' => 100000,
7-
)
8-
);
9-
103
$test_case->populate_url_metrics(
114
array(
125
array(
@@ -17,15 +10,13 @@
1710
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[3][self::DIV]',
1811
'isLCP' => false,
1912
'intersectionRatio' => 0.0,
20-
'intersectionRect' => $outside_viewport_rect,
21-
'boundingClientRect' => $outside_viewport_rect,
13+
'boundingClientRect' => array( 'top' => 100000 ),
2214
),
2315
array(
2416
'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[4][self::DIV]',
2517
'isLCP' => false,
2618
'intersectionRatio' => 0.0,
27-
'intersectionRect' => $outside_viewport_rect,
28-
'boundingClientRect' => $outside_viewport_rect,
19+
'boundingClientRect' => array( 'top' => 100000 ),
2920
),
3021
)
3122
);

plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/buffer.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
<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">
1212
</div>
1313
<p>Pretend this is a super long paragraph that pushes the next image mostly out of the initial viewport.</p>
14-
<img src="https://example.com/bar.jpg" alt="Bar" width="10" height="10" fetchpriority="high" loading="lazy">
14+
<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">
1515
<p>Now the following image is definitely outside the initial viewport.</p>
16-
<img src="https://example.com/baz.jpg" alt="Baz" width="10" height="10" fetchpriority="high">
16+
<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">
1717
<img src="https://example.com/qux.jpg" alt="Qux" width="10" height="10" fetchpriority="high" loading="eager">
1818
<img src="https://example.com/quux.jpg" alt="Quux" width="10" height="10" loading="LAZY"><!-- This one is all good. -->
1919
</div>

plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data/expected.html

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)