-
Notifications
You must be signed in to change notification settings - Fork 98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize lazy-loading of IMG elements in Image Prioritizer #1261
Changes from 7 commits
f50fc6d
58df839
f3fde87
e1c6b37
7c015af
9389a98
9808396
7ab405b
8797919
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,16 +46,9 @@ public function __invoke( OD_HTML_Tag_Walker $walker ): bool { | |
$common_lcp_element = $this->url_metrics_group_collection->get_common_lcp_element(); | ||
if ( ! is_null( $common_lcp_element ) && $xpath === $common_lcp_element['xpath'] ) { | ||
if ( 'high' === $walker->get_attribute( 'fetchpriority' ) ) { | ||
$walker->set_attribute( 'data-od-fetchpriority-already-added', true ); | ||
$walker->set_meta_attribute( 'fetchpriority-already-added', true ); | ||
} else { | ||
$walker->set_attribute( 'fetchpriority', 'high' ); | ||
$walker->set_attribute( 'data-od-added-fetchpriority', true ); | ||
} | ||
|
||
// Never include loading=lazy on the LCP image common across all breakpoints. | ||
if ( 'lazy' === $walker->get_attribute( 'loading' ) ) { | ||
$walker->set_attribute( 'data-od-removed-loading', $walker->get_attribute( 'loading' ) ); | ||
$walker->remove_attribute( 'loading' ); | ||
} | ||
Comment on lines
-55
to
52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code is obsolete because the |
||
} elseif ( is_string( $walker->get_attribute( 'fetchpriority' ) ) && $this->url_metrics_group_collection->is_every_group_populated() ) { | ||
/* | ||
|
@@ -68,12 +61,25 @@ public function __invoke( OD_HTML_Tag_Walker $walker ): bool { | |
* Note also that if this is the LCP element for _some_ of the viewport groups, it will still get | ||
* fetchpriority=high by means of the preload link (with a media query) that is added further below. | ||
*/ | ||
$walker->set_attribute( 'data-od-removed-fetchpriority', $walker->get_attribute( 'fetchpriority' ) ); | ||
$walker->remove_attribute( 'fetchpriority' ); | ||
} | ||
|
||
// TODO: If the image is visible (intersectionRatio!=0) in any of the URL metrics, remove loading=lazy. | ||
// TODO: Conversely, if an image is the LCP element for one breakpoint but not another, add loading=lazy. This won't hurt performance since the image is being preloaded. | ||
$element_max_intersection_ratio = $this->url_metrics_group_collection->get_element_max_intersection_ratio( $xpath ); | ||
|
||
// If the element was not found, we don't know if it was visible for not, so don't do anything. | ||
if ( is_null( $element_max_intersection_ratio ) ) { | ||
$walker->set_meta_attribute( 'unknown-tag', true ); // Mostly useful for debugging why an IMG isn't optimized. | ||
} else { | ||
// Otherwise, make sure visible elements omit the loading attribute, and hidden elements include loading=lazy. | ||
$is_visible = $element_max_intersection_ratio > 0.0; | ||
$loading = (string) $walker->get_attribute( 'loading' ); | ||
if ( $is_visible && 'lazy' === $loading ) { | ||
$walker->remove_attribute( 'loading' ); | ||
} elseif ( ! $is_visible && 'lazy' !== $loading ) { | ||
$walker->set_attribute( 'loading', 'lazy' ); | ||
} | ||
} | ||
// TODO: If an image is visible in one breakpoint but not another, add loading=lazy AND add a regular-priority preload link with media queries (unless LCP in which case it should already have a fetchpriority=high link) so that the image won't be eagerly-loaded for viewports on which it is not shown. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll be filing an issue for this. It may be a bit confusing when looking at the markup because they may well see an LCP There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my testing at #117 (comment) I found that adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've opened this as #1270 |
||
|
||
// If this element is the LCP (for a breakpoint group), add a preload link for it. | ||
foreach ( $this->url_metrics_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,7 +48,7 @@ public function data_provider_test_filter_tag_walker_visitors(): array { | |
<title>...</title> | ||
</head> | ||
<body> | ||
<img data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]" src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" loading="lazy"> | ||
<img data-od-unknown-tag data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]" src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" loading="lazy"> | ||
<script type="module">/* import detect ... */</script> | ||
</body> | ||
</html> | ||
|
@@ -189,7 +189,7 @@ public function data_provider_test_filter_tag_walker_visitors(): array { | |
', | ||
), | ||
|
||
'common-lcp-image-with-fully-populated-sample-data' => array( | ||
'common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data' => array( | ||
'set_up' => function (): void { | ||
$slug = od_get_url_metrics_slug( od_get_normalized_query_vars() ); | ||
$sample_size = od_get_url_metrics_breakpoint_sample_size(); | ||
|
@@ -205,8 +205,24 @@ public function data_provider_test_filter_tag_walker_visitors(): array { | |
'isLCP' => true, | ||
), | ||
array( | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[3][self::IMG]', | ||
'isLCP' => false, | ||
'intersectionRatio' => 0 === $i ? 0.5 : 0.0, // Make sure that the _max_ intersection ratio is considered. | ||
), | ||
array( | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[5][self::IMG]', | ||
'isLCP' => false, | ||
'intersectionRatio' => 0.0, | ||
), | ||
array( | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[6][self::IMG]', | ||
'isLCP' => false, | ||
'intersectionRatio' => 0.0, | ||
), | ||
array( | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[7][self::IMG]', | ||
'isLCP' => false, | ||
'intersectionRatio' => 0.0, | ||
), | ||
) | ||
) | ||
|
@@ -222,7 +238,12 @@ public function data_provider_test_filter_tag_walker_visitors(): array { | |
</head> | ||
<body> | ||
<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" crossorigin="anonymous"> | ||
<img src="https://example.com/bar.jpg" alt="Bar" width="10" height="10" loading="lazy" fetchpriority="high"> | ||
<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"> | ||
<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/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. --> | ||
</body> | ||
</html> | ||
', | ||
|
@@ -235,7 +256,12 @@ public function data_provider_test_filter_tag_walker_visitors(): array { | |
</head> | ||
<body> | ||
<img data-od-added-fetchpriority data-od-removed-loading="lazy" fetchpriority="high" 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" crossorigin="anonymous"> | ||
<img data-od-removed-fetchpriority="high" src="https://example.com/bar.jpg" alt="Bar" width="10" height="10" loading="lazy" > | ||
<p>Pretend this is a super long paragraph that pushes the next image mostly out of the initial viewport.</p> | ||
<img data-od-removed-fetchpriority="high" data-od-removed-loading="lazy" src="https://example.com/bar.jpg" alt="Bar" width="10" height="10" > | ||
<p>Now the following image is definitely outside the initial viewport.</p> | ||
<img data-od-added-loading data-od-removed-fetchpriority="high" loading="lazy" src="https://example.com/baz.jpg" alt="Baz" width="10" height="10" > | ||
<img data-od-removed-fetchpriority="high" data-od-replaced-loading="eager" src="https://example.com/qux.jpg" alt="Qux" width="10" height="10" loading="lazy"> | ||
<img src="https://example.com/quux.jpg" alt="Quux" width="10" height="10" loading="lazy"><!-- This one is all good. --> | ||
Comment on lines
-238
to
+264
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the key test showing how the new functionality is optimizing lazy-loading. Compare this markup with the input markup above. |
||
</body> | ||
</html> | ||
', | ||
|
@@ -286,8 +312,8 @@ public function data_provider_test_filter_tag_walker_visitors(): array { | |
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/foo.jpg" media="screen and (min-width: 783px)"> | ||
</head> | ||
<body> | ||
<img data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]" src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" loading="lazy"> | ||
<img data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]" src="https://example.com/bar.jpg" alt="Bar" width="10" height="10" loading="lazy" fetchpriority="high"> | ||
<img data-od-removed-loading="lazy" data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]" src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800" > | ||
<img data-od-removed-loading="lazy" data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]" src="https://example.com/bar.jpg" alt="Bar" width="10" height="10" fetchpriority="high"> | ||
<script type="module">/* import detect ... */</script> | ||
</body> | ||
</html> | ||
|
@@ -306,7 +332,7 @@ public function data_provider_test_filter_tag_walker_visitors(): array { | |
$viewport_width, | ||
array( | ||
array( | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', // Note: This is intentionally not reflecting the IMG in the HTML below. | ||
'isLCP' => true, | ||
), | ||
) | ||
|
@@ -336,7 +362,7 @@ public function data_provider_test_filter_tag_walker_visitors(): array { | |
</head> | ||
<body> | ||
<script>/* Something injected with wp_body_open */</script> | ||
<img src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800"> | ||
<img data-od-unknown-tag src="https://example.com/foo.jpg" alt="Foo" width="1200" height="800"> | ||
</body> | ||
</html> | ||
', | ||
|
@@ -553,6 +579,7 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { | |
', | ||
), | ||
|
||
// TODO: Eventually the images in this test should all be lazy-loaded, leaving the prioritization to the preload links. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not to be done in this PR. See https://github.com/WordPress/performance/pull/1261/files#r1625015100 |
||
'different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group' => array( | ||
'set_up' => function (): void { | ||
OD_URL_Metrics_Post_Type::store_url_metric( | ||
|
@@ -561,12 +588,14 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { | |
400, | ||
array( | ||
array( | ||
'isLCP' => true, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', | ||
'isLCP' => true, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', | ||
'intersectionRatio' => 1.0, | ||
), | ||
array( | ||
'isLCP' => false, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', | ||
'isLCP' => false, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', | ||
'intersectionRatio' => 0.0, | ||
), | ||
) | ||
) | ||
|
@@ -577,12 +606,14 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { | |
800, | ||
array( | ||
array( | ||
'isLCP' => false, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', | ||
'isLCP' => false, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', | ||
'intersectionRatio' => 0.0, | ||
), | ||
array( | ||
'isLCP' => true, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', | ||
'isLCP' => true, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', | ||
'intersectionRatio' => 1.0, | ||
), | ||
) | ||
) | ||
|
@@ -593,6 +624,7 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { | |
<head> | ||
<meta charset="utf-8"> | ||
<title>...</title> | ||
<style>/* Never show mobile and desktop logos at the same time. */</style> | ||
</head> | ||
<body> | ||
<img src="https://example.com/mobile-logo.png" alt="Mobile Logo" width="600" height="600"> | ||
|
@@ -605,6 +637,7 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { | |
<head> | ||
<meta charset="utf-8"> | ||
<title>...</title> | ||
<style>/* Never show mobile and desktop logos at the same time. */</style> | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/mobile-logo.png" media="screen and (max-width: 480px)"> | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/desktop-logo.png" media="screen and (min-width: 783px)"> | ||
</head> | ||
|
@@ -803,7 +836,7 @@ static function () { | |
<body> | ||
<img src="https://example.com/mobile-logo.png" alt="Mobile Logo" width="600" height="600"> | ||
<p>New paragraph since URL Metrics were captured!</p> | ||
<img src="https://example.com/desktop-logo.png" alt="Desktop Logo" width="600" height="600"> | ||
<img data-od-unknown-tag src="https://example.com/desktop-logo.png" alt="Desktop Logo" width="600" height="600"> | ||
</body> | ||
</html> | ||
', | ||
|
@@ -817,7 +850,7 @@ static function () { | |
<body> | ||
<img data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]" src="https://example.com/mobile-logo.png" alt="Mobile Logo" width="600" height="600"> | ||
<p>New paragraph since URL Metrics were captured!</p> | ||
<img data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[3][self::IMG]" src="https://example.com/desktop-logo.png" alt="Desktop Logo" width="600" height="600"> | ||
<img data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[3][self::IMG]" data-od-unknown-tag src="https://example.com/desktop-logo.png" alt="Desktop Logo" width="600" height="600"> | ||
<script type="module">/* import detect ... */</script> | ||
</body> | ||
</html> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -453,7 +453,29 @@ public function get_attribute( string $name ) { | |
* @return bool Whether an attribute value was set. | ||
*/ | ||
public function set_attribute( string $name, $value ): bool { | ||
return $this->processor->set_attribute( $name, $value ); | ||
$existing_value = $this->processor->get_attribute( $name ); | ||
$result = $this->processor->set_attribute( $name, $value ); | ||
if ( $result ) { | ||
if ( is_string( $existing_value ) ) { | ||
$this->set_meta_attribute( "replaced-{$name}", $existing_value ); | ||
} else { | ||
$this->set_meta_attribute( "added-{$name}", true ); | ||
} | ||
} | ||
return $result; | ||
} | ||
|
||
/** | ||
* Sets a meta attribute. | ||
* | ||
* All meta attributes are prefixed with 'data-od-'. | ||
* | ||
* @param string $name Meta attribute name. | ||
* @param string|true $value Value. | ||
* @return bool Whether an attribute was set. | ||
*/ | ||
public function set_meta_attribute( string $name, $value ): bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I love the term "meta" here, but can't think of a better term |
||
return $this->processor->set_attribute( "data-od-{$name}", $value ); | ||
} | ||
|
||
/** | ||
|
@@ -469,7 +491,12 @@ public function set_attribute( string $name, $value ): bool { | |
* @return bool Whether an attribute was removed. | ||
*/ | ||
public function remove_attribute( string $name ): bool { | ||
return $this->processor->remove_attribute( $name ); | ||
$old_value = $this->processor->get_attribute( $name ); | ||
$result = $this->processor->remove_attribute( $name ); | ||
if ( $result ) { | ||
$this->set_meta_attribute( "removed-{$name}", is_string( $old_value ) ? $old_value : true ); | ||
} | ||
return $result; | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
data-od-
meta attribute is now added automatically when callingset_attribute
on the walker instance.