From 3c0426edfeeea002c4d54e10426d41cc3ec22af1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 25 Jun 2024 16:16:20 -0700 Subject: [PATCH 1/6] Integrate Auto Sizes with Image Prioritizer to ensure correct sizes=auto --- plugins/auto-sizes/auto-sizes.php | 2 + plugins/auto-sizes/optimization-detective.php | 50 ++++++ plugins/auto-sizes/tests/bootstrap.php | 10 ++ .../tests/test-optimization-detective.php | 166 ++++++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 plugins/auto-sizes/optimization-detective.php create mode 100644 plugins/auto-sizes/tests/bootstrap.php create mode 100644 plugins/auto-sizes/tests/test-optimization-detective.php diff --git a/plugins/auto-sizes/auto-sizes.php b/plugins/auto-sizes/auto-sizes.php index b27f810156..04ad2b0996 100644 --- a/plugins/auto-sizes/auto-sizes.php +++ b/plugins/auto-sizes/auto-sizes.php @@ -28,3 +28,5 @@ define( 'IMAGE_AUTO_SIZES_VERSION', '1.0.2' ); require_once __DIR__ . '/hooks.php'; + +require_once __DIR__ . '/optimization-detective.php'; diff --git a/plugins/auto-sizes/optimization-detective.php b/plugins/auto-sizes/optimization-detective.php new file mode 100644 index 0000000000..961146ba01 --- /dev/null +++ b/plugins/auto-sizes/optimization-detective.php @@ -0,0 +1,50 @@ +processor->get_tag() ) { + return false; + } + + $sizes = $context->processor->get_attribute( 'sizes' ); + if ( ! is_string( $sizes ) || 'lazy' !== $context->processor->get_attribute( 'loading' ) ) { + return false; + } + + $sizes = preg_split( '/\s*,\s*/', $sizes ); + if ( is_array( $sizes ) && ! in_array( 'auto', $sizes, true ) ) { + array_unshift( $sizes, 'auto' ); + $context->processor->set_attribute( 'sizes', join( ', ', $sizes ) ); + } + + return true; +} + +/** + * Registers the tag visitor for image tags. + * + * @since n.e.x.t + * + * @param OD_Tag_Visitor_Registry $registry Tag visitor registry. + */ +function auto_sizes_register_tag_visitors( OD_Tag_Visitor_Registry $registry ): void { + $registry->register( 'auto-sizes', 'auto_sizes_visit_tag' ); +} + +// Important: The Image Prioritizer's IMG tag visitor is registered at priority 10, so priority 100 ensures that the loading attribute has been correctly set by the time the Auto Sizes visitor runs. +add_action( 'od_register_tag_visitors', 'auto_sizes_register_tag_visitors', 100 ); diff --git a/plugins/auto-sizes/tests/bootstrap.php b/plugins/auto-sizes/tests/bootstrap.php new file mode 100644 index 0000000000..8c2e84094c --- /dev/null +++ b/plugins/auto-sizes/tests/bootstrap.php @@ -0,0 +1,10 @@ +markTestSkipped( 'Optimization Detective is not active.' ); + } + } + + /** + * Tests auto_sizes_register_tag_visitors(). + * + * @covers ::auto_sizes_register_tag_visitors + */ + public function test_auto_sizes_register_tag_visitors(): void { + if ( ! class_exists( OD_Tag_Visitor_Registry::class ) ) { + $this->markTestSkipped( 'Optimization Detective is not active.' ); + } + $registry = new OD_Tag_Visitor_Registry(); + auto_sizes_register_tag_visitors( $registry ); + $this->assertTrue( $registry->is_registered( 'auto-sizes' ) ); + $this->assertEquals( 'auto_sizes_visit_tag', $registry->get_registered( 'auto-sizes' ) ); + } + + /** + * Data provider. + * + * @return array Data. + */ + public function data_provider_test_od_optimize_template_output_buffer(): array { + return array( + // Note: The Image Prioritizer plugin removes the loading attribute, and so then Auto Sizes does not then add sizes=auto. + 'wrongly_lazy_responsive_img' => array( + 'element_metrics' => array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 1, + ), + 'buffer' => 'Foo', + 'expected' => 'Foo', + ), + + 'non_responsive_image' => array( + 'element_metrics' => array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0, + ), + 'buffer' => 'Quux', + 'expected' => 'Quux', + ), + + 'auto_sizes_added' => array( + 'element_metrics' => array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0, + ), + 'buffer' => 'Foo', + 'expected' => 'Foo', + ), + + 'auto_sizes_already_added' => array( + 'element_metrics' => array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0, + ), + 'buffer' => 'Foo', + 'expected' => 'Foo', + ), + ); + } + + /** + * Test auto_sizes_visit_tag(). + * + * @covers ::auto_sizes_visit_tag + * + * @dataProvider data_provider_test_od_optimize_template_output_buffer + * @throws Exception But it won't. + * @phpstan-param array $element_metrics + */ + public function test_od_optimize_template_output_buffer( array $element_metrics, string $buffer, string $expected ): void { + $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( + $slug, + $this->get_validated_url_metric( + $viewport_width, + array( + $element_metrics, + ) + ) + ); + } + } + + $remove_initial_tabs = static function ( string $input ): string { + return (string) preg_replace( '/^\t+/m', '', $input ); + }; + + $html_start_doc = '...'; + $html_end_doc = ''; + + $expected = $remove_initial_tabs( $expected ); + $buffer = $remove_initial_tabs( $buffer ); + + $buffer = od_optimize_template_output_buffer( $html_start_doc . $buffer . $html_end_doc ); + $buffer = preg_replace( '#.+?]*>#s', '', $buffer ); + $buffer = preg_replace( '#.*$#s', '', $buffer ); + + $this->assertEquals( $expected, $buffer ); + } + + /** + * Gets a validated URL metric. + * + * @param int $viewport_width Viewport width for the URL metric. + * @param array $elements Elements. + * @return OD_URL_Metric URL metric. + * @throws Exception From OD_URL_Metric if there is a parse error, but there won't be. + */ + private function get_validated_url_metric( int $viewport_width, array $elements = array() ): OD_URL_Metric { + $data = array( + 'url' => home_url( '/' ), + 'viewport' => array( + 'width' => $viewport_width, + 'height' => 800, + ), + 'timestamp' => microtime( true ), + 'elements' => array_map( + static function ( array $element ): array { + return array_merge( + array( + 'isLCPCandidate' => true, + 'intersectionRatio' => 1, + 'intersectionRect' => array( + 'width' => 100, + 'height' => 100, + ), + 'boundingClientRect' => array( + 'width' => 100, + 'height' => 100, + ), + ), + $element + ); + }, + $elements + ), + ); + return new OD_URL_Metric( $data ); + } +} From f7b4798c989b094757630bc8bdbbbf268e672944 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 9 Jul 2024 16:06:45 -0700 Subject: [PATCH 2/6] Update readme to explain Image Prioritizer integration and lack of settings --- plugins/auto-sizes/readme.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/auto-sizes/readme.txt b/plugins/auto-sizes/readme.txt index dceb8e1410..fd319e5cfb 100644 --- a/plugins/auto-sizes/readme.txt +++ b/plugins/auto-sizes/readme.txt @@ -12,7 +12,11 @@ Instructs browsers to automatically choose the right image size for lazy-loaded == Description == This plugin implements the HTML spec for adding `sizes="auto"` to lazy-loaded images. -For background, see: https://github.com/whatwg/html/issues/4654. +For background, see the HTML spec issue [Add "auto sizes" for lazy-loaded images](https://github.com/whatwg/html/issues/4654). + +This plugin integrates with the [Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) plugin. When that plugin is active, it starts learning about which images are not in the initial viewport based on actual visitors to your site. When it knows which images are below the fold, it then adds `loading=lazy` to these images. This plugin then extends Image Prioritizer to also add `sizes=auto` to these lazy-loaded images. + +There are currently **no settings** and no user interface for this plugin since it is designed to work without any configuration. == Installation == From 409036102b3e5122413ba76bce5281f3ce15c7c7 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 10 Jul 2024 14:35:51 -0700 Subject: [PATCH 3/6] Add missing since tag Co-authored-by: Mukesh Panchal --- plugins/auto-sizes/optimization-detective.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/auto-sizes/optimization-detective.php b/plugins/auto-sizes/optimization-detective.php index 961146ba01..90ebfe99b9 100644 --- a/plugins/auto-sizes/optimization-detective.php +++ b/plugins/auto-sizes/optimization-detective.php @@ -13,6 +13,8 @@ /** * Visits responsive lazy-loaded IMG tags to ensure they include sizes=auto. * + * @since n.e.x.t + * * @param OD_Tag_Visitor_Context $context Tag visitor context. * @return bool Whether visited. */ From 99a5a03736fa5533523c846662d40b5cf741edf8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 11 Jul 2024 12:18:21 -0700 Subject: [PATCH 4/6] Avoid needlessly tracking element in URL Metrics --- plugins/auto-sizes/optimization-detective.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auto-sizes/optimization-detective.php b/plugins/auto-sizes/optimization-detective.php index 90ebfe99b9..d085634c7b 100644 --- a/plugins/auto-sizes/optimization-detective.php +++ b/plugins/auto-sizes/optimization-detective.php @@ -34,7 +34,7 @@ function auto_sizes_visit_tag( OD_Tag_Visitor_Context $context ): bool { $context->processor->set_attribute( 'sizes', join( ', ', $sizes ) ); } - return true; + return false; // Since this tag visitor does not require this tag to be included in the URL Metrics. } /** From 83cabaf7dabcc8c7927e72b3e8bd9bdc5ccf361c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 11 Jul 2024 12:18:47 -0700 Subject: [PATCH 5/6] Clarify current purpose of return value --- plugins/auto-sizes/optimization-detective.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auto-sizes/optimization-detective.php b/plugins/auto-sizes/optimization-detective.php index d085634c7b..afe9b54d42 100644 --- a/plugins/auto-sizes/optimization-detective.php +++ b/plugins/auto-sizes/optimization-detective.php @@ -16,7 +16,7 @@ * @since n.e.x.t * * @param OD_Tag_Visitor_Context $context Tag visitor context. - * @return bool Whether visited. + * @return false Whether the tag should be recorded in URL metrics. */ function auto_sizes_visit_tag( OD_Tag_Visitor_Context $context ): bool { if ( 'IMG' !== $context->processor->get_tag() ) { From f5e4231ca712ab9f8e1ee2c3741115decf0c24f3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 14 Jul 2024 22:02:20 -0700 Subject: [PATCH 6/6] Remove sizes=auto if Image Prioritizer removed lazy-loading --- plugins/auto-sizes/optimization-detective.php | 18 ++++++++++++++++-- .../tests/test-optimization-detective.php | 19 +++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/plugins/auto-sizes/optimization-detective.php b/plugins/auto-sizes/optimization-detective.php index afe9b54d42..7b265edf5d 100644 --- a/plugins/auto-sizes/optimization-detective.php +++ b/plugins/auto-sizes/optimization-detective.php @@ -24,13 +24,27 @@ function auto_sizes_visit_tag( OD_Tag_Visitor_Context $context ): bool { } $sizes = $context->processor->get_attribute( 'sizes' ); - if ( ! is_string( $sizes ) || 'lazy' !== $context->processor->get_attribute( 'loading' ) ) { + if ( ! is_string( $sizes ) ) { return false; } $sizes = preg_split( '/\s*,\s*/', $sizes ); - if ( is_array( $sizes ) && ! in_array( 'auto', $sizes, true ) ) { + if ( ! is_array( $sizes ) ) { + return false; + } + + $is_lazy_loaded = ( 'lazy' === $context->processor->get_attribute( 'loading' ) ); + $has_auto_sizes = in_array( 'auto', $sizes, true ); + + $changed = false; + if ( $is_lazy_loaded && ! $has_auto_sizes ) { array_unshift( $sizes, 'auto' ); + $changed = true; + } elseif ( ! $is_lazy_loaded && $has_auto_sizes ) { + $sizes = array_diff( $sizes, array( 'auto' ) ); + $changed = true; + } + if ( $changed ) { $context->processor->set_attribute( 'sizes', join( ', ', $sizes ) ); } diff --git a/plugins/auto-sizes/tests/test-optimization-detective.php b/plugins/auto-sizes/tests/test-optimization-detective.php index ef1f2c5c09..7fab403b4d 100644 --- a/plugins/auto-sizes/tests/test-optimization-detective.php +++ b/plugins/auto-sizes/tests/test-optimization-detective.php @@ -39,7 +39,7 @@ public function test_auto_sizes_register_tag_visitors(): void { public function data_provider_test_od_optimize_template_output_buffer(): array { return array( // Note: The Image Prioritizer plugin removes the loading attribute, and so then Auto Sizes does not then add sizes=auto. - 'wrongly_lazy_responsive_img' => array( + 'wrongly_lazy_responsive_img' => array( 'element_metrics' => array( 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', 'isLCP' => false, @@ -49,7 +49,7 @@ public function data_provider_test_od_optimize_template_output_buffer(): array { 'expected' => 'Foo', ), - 'non_responsive_image' => array( + 'non_responsive_image' => array( 'element_metrics' => array( 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', 'isLCP' => false, @@ -59,7 +59,7 @@ public function data_provider_test_od_optimize_template_output_buffer(): array { 'expected' => 'Quux', ), - 'auto_sizes_added' => array( + 'auto_sizes_added' => array( 'element_metrics' => array( 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', 'isLCP' => false, @@ -69,7 +69,7 @@ public function data_provider_test_od_optimize_template_output_buffer(): array { 'expected' => 'Foo', ), - 'auto_sizes_already_added' => array( + 'auto_sizes_already_added' => array( 'element_metrics' => array( 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', 'isLCP' => false, @@ -78,6 +78,17 @@ public function data_provider_test_od_optimize_template_output_buffer(): array { 'buffer' => 'Foo', 'expected' => 'Foo', ), + + // If Auto Sizes added the sizes=auto attribute but Image Prioritizer ended up removing it due to the image not being lazy-loaded, remove sizes=auto again. + 'wrongly_auto_sized_responsive_img' => array( + 'element_metrics' => array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 1, + ), + 'buffer' => 'Foo', + 'expected' => 'Foo', + ), ); }