Skip to content

Commit

Permalink
Fix handling images with JS-based lazy-loading
Browse files Browse the repository at this point in the history
  • Loading branch information
westonruter committed Dec 16, 2024
1 parent d6c07ef commit b2d972c
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
*/
final class Image_Prioritizer_Img_Tag_Visitor extends Image_Prioritizer_Tag_Visitor {

/**
* List of PICTURE XPaths to skip processing of child IMG tags.
*
* @since n.e.x.t
* @var string[]
*/
private $picture_ancestor_xpaths_to_skip = array();

/**
* Visits a tag.
*
Expand All @@ -35,14 +43,50 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool {
$tag = $processor->get_tag();

if ( 'PICTURE' === $tag ) {
return $this->process_picture( $processor, $context );
$picture_xpath = $processor->get_xpath();
if ( false === $this->process_picture( $processor, $context ) ) {
$this->picture_ancestor_xpaths_to_skip[] = $picture_xpath;
}
return false; // Because the IMG child is what gets tracked in URL Metrics.
} elseif ( 'IMG' === $tag ) {
return $this->process_img( $processor, $context );
}

return false;
}

/**
* Determines whether the current IMG is valid for tracking in URL Metrics.
*
* An IMG must have a src attribute which is not a data: URL. And if it has a srcset attribute, it also must not be
* a data: URL.
*
* @since n.e.x.t
*
* @param OD_HTML_Tag_Processor $processor Tag Processor.
* @return bool Whether valid for tracking in URL Metrics.
*/
private function is_img_with_valid_src_and_srcset( OD_HTML_Tag_Processor $processor ): bool {
$src = $this->get_attribute_value( $processor, 'src' );
$has_src = ( is_string( $src ) && '' !== $src );
if ( ! $has_src ) {
return false;
}

$srcset = $this->get_attribute_value( $processor, 'srcset' );
$has_srcset = ( is_string( $srcset ) && '' !== $srcset );

// Abort data: URLs (which may very be JS-based lazy-loading).
if ( $this->is_data_url( $src ) ) {
return false;
}
if ( $has_srcset && $this->is_data_url( $srcset ) ) {
return false;
}

return true;
}

/**
* Process an IMG element.
*
Expand All @@ -53,13 +97,21 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool {
* @return bool Whether the tag should be tracked in URL Metrics.
*/
private function process_img( OD_HTML_Tag_Processor $processor, OD_Tag_Visitor_Context $context ): bool {
$src = $this->get_valid_src( $processor );
if ( null === $src ) {
if ( ! $this->is_img_with_valid_src_and_srcset( $processor ) ) {
return false;
}

$xpath = $processor->get_xpath();

// If the PICTURE's processing was aborted, then abort processing its child IMG as well.
if ( 'PICTURE' === $this->get_parent_tag_name( $context ) ) {
foreach ( $this->picture_ancestor_xpaths_to_skip as $picture_xpath ) {
if ( str_starts_with( $xpath, $picture_xpath ) ) {
return false;
}
}
}

$current_fetchpriority = $this->get_attribute_value( $processor, 'fetchpriority' );
$is_lazy_loaded = 'lazy' === $this->get_attribute_value( $processor, 'loading' );
$updated_fetchpriority = null;
Expand Down Expand Up @@ -187,7 +239,7 @@ private function process_img( OD_HTML_Tag_Processor $processor, OD_Tag_Visitor_C
*
* @param OD_HTML_Tag_Processor $processor HTML tag processor.
* @param OD_Tag_Visitor_Context $context Tag visitor context.
* @return bool Whether the tag should be tracked in URL Metrics.
* @return bool Whether the PICTURE was processed.
*/
private function process_picture( OD_HTML_Tag_Processor $processor, OD_Tag_Visitor_Context $context ): bool {
/**
Expand Down Expand Up @@ -218,8 +270,13 @@ private function process_picture( OD_HTML_Tag_Processor $processor, OD_Tag_Visit
}

// Abort processing if a SOURCE lacks the required srcset attribute.
$srcset = $this->get_valid_src( $processor, 'srcset' );
if ( null === $srcset ) {
$srcset = $this->get_attribute_value( $processor, 'srcset' );
if ( ! is_string( $srcset ) ) {
return false;
}

// Abort if the srcset is a data: URL since there is nothing to optimize.
if ( $this->is_data_url( $srcset ) ) {
return false;
}

Expand All @@ -242,8 +299,8 @@ private function process_picture( OD_HTML_Tag_Processor $processor, OD_Tag_Visit

// Process the IMG element within the PICTURE.
if ( 'IMG' === $tag && ! $processor->is_tag_closer() ) {
$src = $this->get_valid_src( $processor );
if ( null === $src ) {
// Abort if process_img() won't later be processing this IMG.
if ( ! $this->is_img_with_valid_src_and_srcset( $processor ) ) {
return false;
}

Expand Down Expand Up @@ -274,31 +331,7 @@ private function process_picture( OD_HTML_Tag_Processor $processor, OD_Tag_Visit
)
);

return false;
}

/**
* Gets valid src attribute value for preloading.
*
* Returns null if the src attribute is not a string (i.e. src was used as a boolean attribute was used), if it
* it has an empty string value after trimming, or if it is a data: URL.
*
* @since n.e.x.t
*
* @param OD_HTML_Tag_Processor $processor Processor.
* @param 'src'|'srcset' $attribute_name Attribute name.
* @return non-empty-string|null URL which is not a data: URL.
*/
private function get_valid_src( OD_HTML_Tag_Processor $processor, string $attribute_name = 'src' ): ?string {
$src = $processor->get_attribute( $attribute_name );
if ( ! is_string( $src ) ) {
return null;
}
$src = trim( $src );
if ( '' === $src || $this->is_data_url( $src ) ) {
return null;
}
return $src;
return true;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/**
* Tag visitor that optimizes image tags.
*
* @phpstan-type NormalizedAttributeNames 'fetchpriority'|'loading'|'crossorigin'|'preload'|'referrerpolicy'|'type'
* @phpstan-type NormalizedAttributeNames 'src'|'srcset'|'fetchpriority'|'loading'|'crossorigin'|'preload'|'referrerpolicy'|'type'
*
* @since 0.1.0
* @access private
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
<?php
return array(
'set_up' => static function ( Test_Image_Prioritizer_Helper $test_case ): void {
$slug = od_get_url_metrics_slug( od_get_normalized_query_vars() );
'set_up' => static function (): void {},

// Populate one URL Metric so that none of the IMG elements are unknown.
OD_URL_Metrics_Post_Type::store_url_metric(
$slug,
$test_case->get_sample_url_metric(
array(
'viewport_width' => 1000,
'elements' => array(
array(
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
'isLCP' => true,
),
array(
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]',
'isLCP' => false,
),
),
)
)
);
},
/*
* Example 1 comes from Avada's Fusion_Images lazy images which replaces the srcset attribute with data-srcset but
* which leaves the src attribute as-is.
*
* Example 2 comes from Speed Optimizer v7.7.2 by Site Ground which uses lazysizes v5.3.1.
* See <https://plugins.trac.wordpress.org/browser/sg-cachepress/tags/7.7.2/core/Lazy_Load/Lazy_Load_Images.php>.
*/
'buffer' => '
<html lang="en">
<head>
Expand All @@ -31,7 +17,7 @@
<script>/* custom lazy-loading */</script>
</head>
<body>
<!-- Example seen in the wild which caused a preload link to be added with a data: URL in the imagesrcset attribute. -->
<!-- Example 1 -->
<img
src="https://example.com/foo.webp"
data-orig-src="https://example.com/foo.webp"
Expand All @@ -44,7 +30,21 @@ class="lazyload wp-image-1"
data-orig-sizes="(max-width: 767px) 100vw, 1920px"
>
<!-- Example with an adjoining NOSCRIPT > IMG tag which should be excluded from URL Metrics. -->
<!-- Example 1 extended to PICTURE, where none of these should be tracked in URL Metrics -->
<picture>
<source type="image/avif" srcset="https://example.com/foo-300x225.avif 300w, https://example.com/foo-1024x768.avif 1024w, https://example.com/foo-768x576.avif 768w, https://example.com/foo-1536x1152.avif 1536w, https://example.com/foo-2048x1536.avif 2048w" sizes="(max-width: 600px) 480px, 800px">
<img class="lazyload" width="1200" height="800" src="https://example.com/foo.avif" alt="Foo" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 600px) 480px, 800px">
</picture>
<picture>
<source type="image/avif" srcset="https://example.com/foo-300x225.avif 300w, https://example.com/foo-1024x768.avif 1024w, https://example.com/foo-768x576.avif 768w, https://example.com/foo-1536x1152.avif 1536w, https://example.com/foo-2048x1536.avif 2048w" sizes="(max-width: 600px) 480px, 800px">
<img class="lazyload" width="1200" height="800" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Foo">
</picture>
<picture>
<source type="image/avif" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 600px) 480px, 800px">
<img class="lazyload" width="1200" height="800" src="https://example.com/foo.avif" alt="Foo">
</picture>
<!-- Example 2 -->
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-src="https://example.com/bar.jpg" data-srcset="https://example.com/bar-large.jpg 1000w, https://example.com/bar-large.jpg 1000w" sizes="(max-width: 556px) 100vw, 556px" alt="Bar" class="attachment-large size-large wp-image-2 has-transparency lazyload" width="500" height="300">
<noscript>
<img src="https://example.com/bar.jpg" srcset="https://example.com/bar-large.jpg 1000w, https://example.com/bar-large.jpg 1000w" sizes="(max-width: 556px) 100vw, 556px" alt="Bar" class="attachment-large size-large wp-image-2 has-transparency lazyload" width="500" height="300">
Expand All @@ -58,11 +58,10 @@ class="lazyload wp-image-1"
<meta charset="utf-8">
<title>...</title>
<script>/* custom lazy-loading */</script>
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/foo.webp" imagesrcset="data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%271000%27%20height%3D%27800%27%20viewBox%3D%270%200%201000%20800%27%3E%3Crect%20width%3D%271000%27%20height%3D%27800%27%20fill-opacity%3D%220%22%2F%3E%3C%2Fsvg%3E" media="screen and (min-width: 783px)">
</head>
<body>
<!-- Example seen in the wild which caused a preload link to be added with a data: URL in the imagesrcset attribute. -->
<img data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]"
<!-- Example 1 -->
<img
src="https://example.com/foo.webp"
data-orig-src="https://example.com/foo.webp"
width="1000"
Expand All @@ -74,7 +73,21 @@ class="lazyload wp-image-1"
data-orig-sizes="(max-width: 767px) 100vw, 1920px"
>
<!-- Example with an adjoining NOSCRIPT > IMG tag which should be excluded from URL Metrics. -->
<!-- Example 1 extended to PICTURE, where none of these should be tracked in URL Metrics -->
<picture>
<source type="image/avif" srcset="https://example.com/foo-300x225.avif 300w, https://example.com/foo-1024x768.avif 1024w, https://example.com/foo-768x576.avif 768w, https://example.com/foo-1536x1152.avif 1536w, https://example.com/foo-2048x1536.avif 2048w" sizes="(max-width: 600px) 480px, 800px">
<img class="lazyload" width="1200" height="800" src="https://example.com/foo.avif" alt="Foo" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 600px) 480px, 800px">
</picture>
<picture>
<source type="image/avif" srcset="https://example.com/foo-300x225.avif 300w, https://example.com/foo-1024x768.avif 1024w, https://example.com/foo-768x576.avif 768w, https://example.com/foo-1536x1152.avif 1536w, https://example.com/foo-2048x1536.avif 2048w" sizes="(max-width: 600px) 480px, 800px">
<img class="lazyload" width="1200" height="800" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Foo">
</picture>
<picture>
<source type="image/avif" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 600px) 480px, 800px">
<img class="lazyload" width="1200" height="800" src="https://example.com/foo.avif" alt="Foo">
</picture>
<!-- Example 2 -->
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-src="https://example.com/bar.jpg" data-srcset="https://example.com/bar-large.jpg 1000w, https://example.com/bar-large.jpg 1000w" sizes="(max-width: 556px) 100vw, 556px" alt="Bar" class="attachment-large size-large wp-image-2 has-transparency lazyload" width="500" height="300">
<noscript>
<img src="https://example.com/bar.jpg" srcset="https://example.com/bar-large.jpg 1000w, https://example.com/bar-large.jpg 1000w" sizes="(max-width: 556px) 100vw, 556px" alt="Bar" class="attachment-large size-large wp-image-2 has-transparency lazyload" width="500" height="300">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
<?php
return array(
'set_up' => static function ( Test_Image_Prioritizer_Helper $test_case ): void {
$breakpoint_max_widths = array( 480, 600, 782 );

add_filter(
'od_breakpoint_max_widths',
static function () use ( $breakpoint_max_widths ) {
return $breakpoint_max_widths;
}
);

$test_case->populate_url_metrics(
array(
array(
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::PICTURE]/*[2][self::IMG]',
'isLCP' => true,
),
)
);
},
'set_up' => static function ( Test_Image_Prioritizer_Helper $test_case ): void {},
'buffer' => '
<html lang="en">
<head>
Expand All @@ -42,8 +24,9 @@ static function () use ( $breakpoint_max_widths ) {
<body>
<picture>
<source type="image/avif" media="(max-width: 600px)" srcset="https://example.com/foo-300x225.avif 300w, https://example.com/foo-1024x768.avif 1024w, https://example.com/foo-768x576.avif 768w, https://example.com/foo-1536x1152.avif 1536w, https://example.com/foo-2048x1536.avif 2048w" sizes="(max-width: 600px) 480px, 800px">
<img data-od-fetchpriority-already-added fetchpriority="high" decoding="async" width="1200" height="800" src="https://example.com/foo.jpg" alt="Foo" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 600px) 480px, 800px">
<img fetchpriority="high" decoding="async" width="1200" height="800" src="https://example.com/foo.jpg" alt="Foo" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 600px) 480px, 800px">
</picture>
<script type="module">/* import detect ... */</script>
</body>
</html>
',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
<?php
return array(
'set_up' => static function ( Test_Image_Prioritizer_Helper $test_case ): void {
$breakpoint_max_widths = array( 480, 600, 782 );

add_filter(
'od_breakpoint_max_widths',
static function () use ( $breakpoint_max_widths ) {
return $breakpoint_max_widths;
}
);

$test_case->populate_url_metrics(
array(
array(
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::PICTURE]/*[2][self::IMG]',
'isLCP' => true,
),
)
);
},
'set_up' => static function (): void {},
'buffer' => '
<html lang="en">
<head>
Expand All @@ -42,8 +24,9 @@ static function () use ( $breakpoint_max_widths ) {
<body>
<picture>
<source srcset="https://example.com/foo-300x225.avif 300w, https://example.com/foo-1024x768.avif 1024w, https://example.com/foo-768x576.avif 768w, https://example.com/foo-1536x1152.avif 1536w, https://example.com/foo-2048x1536.avif 2048w" sizes="(max-width: 600px) 480px, 800px">
<img data-od-fetchpriority-already-added fetchpriority="high" decoding="async" width="1200" height="800" src="https://example.com/foo.jpg" alt="Foo" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 600px) 480px, 800px">
<img fetchpriority="high" decoding="async" width="1200" height="800" src="https://example.com/foo.jpg" alt="Foo" srcset="https://example.com/foo-300x225.jpg 300w, https://example.com/foo-1024x768.jpg 1024w, https://example.com/foo-768x576.jpg 768w, https://example.com/foo-1536x1152.jpg 1536w, https://example.com/foo-2048x1536.jpg 2048w" sizes="(max-width: 600px) 480px, 800px">
</picture>
<script type="module">/* import detect ... */</script>
</body>
</html>
',
Expand Down

0 comments on commit b2d972c

Please sign in to comment.