Skip to content

Commit

Permalink
Merge pull request #1322 from WordPress/add/auto-sizes-image-prioriti…
Browse files Browse the repository at this point in the history
…zer-integration

Integrate Auto Sizes with Image Prioritizer to ensure correct sizes=auto
  • Loading branch information
joemcgill authored Jul 15, 2024
2 parents 9298f62 + f5e4231 commit 622ecde
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 0 deletions.
2 changes: 2 additions & 0 deletions plugins/auto-sizes/auto-sizes.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@
define( 'IMAGE_AUTO_SIZES_VERSION', '1.0.2' );

require_once __DIR__ . '/hooks.php';

require_once __DIR__ . '/optimization-detective.php';
66 changes: 66 additions & 0 deletions plugins/auto-sizes/optimization-detective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
/**
* Optimization Detective extensions by Auto Sizes.
*
* @since n.e.x.t
* @package auto-sizes
*/

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

/**
* 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 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() ) {
return false;
}

$sizes = $context->processor->get_attribute( 'sizes' );
if ( ! is_string( $sizes ) ) {
return false;
}

$sizes = preg_split( '/\s*,\s*/', $sizes );
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 ) );
}

return false; // Since this tag visitor does not require this tag to be included in the URL Metrics.
}

/**
* 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 );
2 changes: 2 additions & 0 deletions plugins/auto-sizes/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ This plugin implements experimental enhancements for the responsive images funct
1. Improvements to the accuracy of the `sizes` attribute by using available layout information in the theme.
2. Implementation of the new HTML spec for adding `sizes="auto"` to lazy-loaded images. 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 ==
Expand Down
10 changes: 10 additions & 0 deletions plugins/auto-sizes/tests/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
/**
* Test Bootstrap for Auto Sizes.
*
* @package auto-sizes
*/

// Require the suggested plugins.
require_once __DIR__ . '/../../optimization-detective/load.php';
require_once __DIR__ . '/../../image-prioritizer/load.php';
177 changes: 177 additions & 0 deletions plugins/auto-sizes/tests/test-optimization-detective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<?php
/**
* Tests for auto-sizes plugin's optimization-detective.php.
*
* @package auto-sizes
*/

class Test_Auto_Sizes_Optimization_Detective extends WP_UnitTestCase {
/**
* Runs the routine before each test is executed.
*/
public function set_up(): void {
parent::set_up();
if ( ! defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) {
$this->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<string, mixed> 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' => '<img src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" loading="lazy" srcset="https://example.com/foo-480w.jpg 480w, https://example.com/foo-800w.jpg 800w" sizes="(max-width: 600px) 480px, 800px">',
'expected' => '<img data-od-removed-loading="lazy" src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" srcset="https://example.com/foo-480w.jpg 480w, https://example.com/foo-800w.jpg 800w" sizes="(max-width: 600px) 480px, 800px">',
),

'non_responsive_image' => array(
'element_metrics' => array(
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
'isLCP' => false,
'intersectionRatio' => 0,
),
'buffer' => '<img src="https://example.com/foo.jpg" alt="Quux" width="1200" height="800" loading="lazy">',
'expected' => '<img src="https://example.com/foo.jpg" alt="Quux" width="1200" height="800" loading="lazy">',
),

'auto_sizes_added' => array(
'element_metrics' => array(
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
'isLCP' => false,
'intersectionRatio' => 0,
),
'buffer' => '<img src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" loading="lazy" srcset="https://example.com/foo-480w.jpg 480w, https://example.com/foo-800w.jpg 800w" sizes="(max-width: 600px) 480px, 800px">',
'expected' => '<img data-od-replaced-sizes="(max-width: 600px) 480px, 800px" src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" loading="lazy" srcset="https://example.com/foo-480w.jpg 480w, https://example.com/foo-800w.jpg 800w" sizes="auto, (max-width: 600px) 480px, 800px">',
),

'auto_sizes_already_added' => array(
'element_metrics' => array(
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
'isLCP' => false,
'intersectionRatio' => 0,
),
'buffer' => '<img src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" loading="lazy" srcset="https://example.com/foo-480w.jpg 480w, https://example.com/foo-800w.jpg 800w" sizes="auto, (max-width: 600px) 480px, 800px">',
'expected' => '<img src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" loading="lazy" srcset="https://example.com/foo-480w.jpg 480w, https://example.com/foo-800w.jpg 800w" sizes="auto, (max-width: 600px) 480px, 800px">',
),

// 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' => '<img src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" loading="lazy" srcset="https://example.com/foo-480w.jpg 480w, https://example.com/foo-800w.jpg 800w" sizes="auto, (max-width: 600px) 480px, 800px">',
'expected' => '<img data-od-replaced-sizes="auto, (max-width: 600px) 480px, 800px" data-od-removed-loading="lazy" src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" srcset="https://example.com/foo-480w.jpg 480w, https://example.com/foo-800w.jpg 800w" sizes="(max-width: 600px) 480px, 800px">',
),
);
}

/**
* 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<string, mixed> $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 lang="en"><head><meta charset="utf-8"><title>...</title></head><body>';
$html_end_doc = '</body></html>';

$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( '#.+?<body[^>]*>#s', '', $buffer );
$buffer = preg_replace( '#</body>.*$#s', '', $buffer );

$this->assertEquals( $expected, $buffer );
}

/**
* Gets a validated URL metric.
*
* @param int $viewport_width Viewport width for the URL metric.
* @param array<array{xpath: string, isLCP: bool}> $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 );
}
}

0 comments on commit 622ecde

Please sign in to comment.