-
Notifications
You must be signed in to change notification settings - Fork 101
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
Lazy load videos and video posters #1596
Changes from 10 commits
842915b
4dd7fea
fbfde55
2a20c06
a672216
6fcf174
09d335c
f0a4b14
bb2323d
88a6678
9714de3
bde289f
75a56fa
b7ab0db
da8aa56
c8b7ce0
ed47b47
2ae1b06
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 |
---|---|---|
|
@@ -21,6 +21,12 @@ | |
* @access private | ||
*/ | ||
final class Image_Prioritizer_Video_Tag_Visitor extends Image_Prioritizer_Tag_Visitor { | ||
/** | ||
* Whether the lazy-loading script was added to the body. | ||
* | ||
* @var bool | ||
*/ | ||
protected $added_lazy_script = false; | ||
|
||
/** | ||
* Visits a tag. | ||
|
@@ -35,18 +41,16 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { | |
return false; | ||
} | ||
|
||
// TODO: If $context->url_metric_group_collection->get_element_max_intersection_ratio( $xpath ) is 0.0, then the video is not in any initial viewport and the VIDEO tag could get the preload=none attribute added. | ||
|
||
$poster = $this->get_poster( $context ); | ||
|
||
if ( null !== $poster ) { | ||
$this->reduce_poster_image_size( $poster, $context ); | ||
$this->preload_poster_image( $poster, $context ); | ||
|
||
return true; | ||
} | ||
|
||
return false; | ||
$this->lazy_load_videos( $poster, $context ); | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
|
@@ -129,7 +133,7 @@ private function preload_poster_image( string $poster, OD_Tag_Visitor_Context $c | |
|
||
$xpath = $processor->get_xpath(); | ||
|
||
// If this element is the LCP (for a breakpoint group), add a preload link for it. | ||
// If this element is the LCP (for a breakpoint group), add a preload link for the poster image. | ||
foreach ( $context->url_metric_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) { | ||
$link_attributes = array( | ||
'rel' => 'preload', | ||
|
@@ -151,4 +155,56 @@ private function preload_poster_image( string $poster, OD_Tag_Visitor_Context $c | |
); | ||
} | ||
} | ||
|
||
/** | ||
* Adjusts `autoplay` and `preload` values for videos outside initial viewport. | ||
* | ||
* @since n.e.x.t | ||
* | ||
* @param non-empty-string|null $poster Poster image URL. | ||
* @param OD_Tag_Visitor_Context $context Tag visitor context, with the cursor currently at an embed block. | ||
*/ | ||
private function lazy_load_videos( ?string $poster, OD_Tag_Visitor_Context $context ): void { | ||
$processor = $context->processor; | ||
|
||
$xpath = $processor->get_xpath(); | ||
|
||
$intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( $xpath ); | ||
|
||
// Set preload="auto" if the video is the LCP element among all viewports. | ||
$common_lcp_element = $context->url_metric_group_collection->get_common_lcp_element(); | ||
if ( null !== $common_lcp_element && $xpath === $common_lcp_element['xpath'] ) { | ||
$processor->set_attribute( 'preload', 'auto' ); | ||
return; | ||
} | ||
|
||
// TODO: What if URL metrics aren't available for all viewports yet? | ||
if ( $intersection_ratio > 0 ) { | ||
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 wonder if I realize this may be a problem for lazy-loaded images and embeds as well. To deal with this, the lazy-loading optimization should perhaps depend on the smallest viewport group (mobile) being populated as well as the largest group (desktop). But if a site only ever gets mobile or desktop visitors, then this could indefinitely delay the optimization from being applied, which isn't good. Since this is a wholistic problem/concern/question for videos, images, and embeds, it probably isn't something needing to be addressed in this PR and we can open an issue to consider further. Related: #1595 (comment) For prioritizing the loading of LCP images the situation is a bit different since we aren't dependent on the one Also related #1404 |
||
return; | ||
} | ||
|
||
$preload = $processor->get_attribute( 'preload' ); | ||
if ( 'none' !== $preload ) { | ||
$processor->set_attribute( 'data-original-preload', null !== $preload ? $preload : 'default' ); | ||
$processor->set_attribute( 'preload', 'none' ); | ||
swissspidy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$processor->add_class( 'wp-lazy-video' ); | ||
} | ||
|
||
if ( null !== $processor->get_attribute( 'autoplay' ) ) { | ||
$processor->set_attribute( 'data-original-autoplay', true ); | ||
$processor->remove_attribute( 'autoplay' ); | ||
swissspidy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$processor->add_class( 'wp-lazy-video' ); | ||
} | ||
|
||
if ( null !== $poster ) { | ||
$processor->set_attribute( 'data-original-poster', $poster ); | ||
$processor->remove_attribute( 'poster' ); | ||
swissspidy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$processor->add_class( 'wp-lazy-video' ); | ||
} | ||
|
||
if ( ! $this->added_lazy_script ) { | ||
$processor->append_body_html( wp_get_inline_script_tag( image_prioritizer_get_lazy_load_script(), array( 'type' => 'module' ) ) ); | ||
$this->added_lazy_script = true; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
<?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; | ||
} | ||
); | ||
|
||
foreach ( array_merge( $breakpoint_max_widths, array( 1000 ) ) as $viewport_width ) { | ||
OD_URL_Metrics_Post_Type::store_url_metric( | ||
od_get_url_metrics_slug( od_get_normalized_query_vars() ), | ||
$test_case->get_sample_url_metric( | ||
array( | ||
'viewport_width' => $viewport_width, | ||
'elements' => array( | ||
array( | ||
'isLCP' => true, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::VIDEO]', | ||
'boundingClientRect' => $test_case->get_sample_dom_rect(), | ||
'intersectionRatio' => 1.0, | ||
), | ||
array( | ||
'isLCP' => false, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]', | ||
'boundingClientRect' => $test_case->get_sample_dom_rect(), | ||
'intersectionRatio' => 0.1, | ||
), | ||
array( | ||
'isLCP' => false, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[3][self::VIDEO]', | ||
'boundingClientRect' => $test_case->get_sample_dom_rect(), | ||
'intersectionRatio' => 0.0, | ||
), | ||
array( | ||
'isLCP' => false, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[4][self::VIDEO]', | ||
'boundingClientRect' => $test_case->get_sample_dom_rect(), | ||
'intersectionRatio' => 0.0, | ||
), | ||
), | ||
) | ||
) | ||
); | ||
} | ||
}, | ||
'buffer' => ' | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>...</title> | ||
</head> | ||
<body> | ||
<video class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="none" autoplay></video> | ||
<video class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="auto" autoplay></video> | ||
<video class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="auto" autoplay></video> | ||
<video class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="metadata" autoplay></video> | ||
<video class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" autoplay></video> | ||
</body> | ||
</html> | ||
', | ||
'expected' => ' | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>...</title> | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/poster.jpg" media="screen"> | ||
</head> | ||
<body> | ||
<video data-od-replaced-preload="none" data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[1][self::VIDEO]" class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="auto" autoplay></video> | ||
<video data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]" class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="auto" autoplay></video> | ||
<video data-od-added-data-original-autoplay data-od-added-data-original-poster data-od-added-data-original-preload data-od-removed-autoplay data-od-removed-poster="https://example.com/poster.jpg" data-od-replaced-class="desktop" data-od-replaced-preload="auto" data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[3][self::VIDEO]" data-original-autoplay data-original-poster="https://example.com/poster.jpg" data-original-preload="auto" class="desktop wp-lazy-video" width="1200" height="500" src="https://example.com/header.mp4" preload="none" ></video> | ||
<video data-od-added-data-original-autoplay data-od-added-data-original-poster data-od-added-data-original-preload data-od-removed-autoplay data-od-removed-poster="https://example.com/poster.jpg" data-od-replaced-class="desktop" data-od-replaced-preload="metadata" data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[4][self::VIDEO]" data-original-autoplay data-original-poster="https://example.com/poster.jpg" data-original-preload="metadata" class="desktop wp-lazy-video" width="1200" height="500" src="https://example.com/header.mp4" preload="none" ></video> | ||
<video data-od-added-data-original-autoplay data-od-added-data-original-poster data-od-added-data-original-preload data-od-added-preload data-od-removed-autoplay data-od-removed-poster="https://example.com/poster.jpg" data-od-replaced-class="desktop" data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[5][self::VIDEO]" data-original-autoplay data-original-poster="https://example.com/poster.jpg" data-original-preload="default" preload="none" class="desktop wp-lazy-video" width="1200" height="500" src="https://example.com/header.mp4" ></video> | ||
<script type="module">/* const lazyVideoObserver ... */</script> | ||
<script type="module">/* import detect ... */</script> | ||
</body> | ||
</html> | ||
', | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
<?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; | ||
} | ||
); | ||
|
||
foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) { | ||
OD_URL_Metrics_Post_Type::store_url_metric( | ||
od_get_url_metrics_slug( od_get_normalized_query_vars() ), | ||
$test_case->get_sample_url_metric( | ||
array( | ||
'viewport_width' => $non_desktop_viewport_width, | ||
'elements' => array( | ||
array( | ||
'isLCP' => true, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::VIDEO]', | ||
'boundingClientRect' => $test_case->get_sample_dom_rect(), | ||
'intersectionRatio' => 1.0, | ||
), | ||
array( | ||
'isLCP' => false, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]', | ||
'boundingClientRect' => $test_case->get_sample_dom_rect(), | ||
'intersectionRatio' => 0.1, | ||
), | ||
array( | ||
'isLCP' => false, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[3][self::VIDEO]', | ||
'boundingClientRect' => $test_case->get_sample_dom_rect(), | ||
'intersectionRatio' => 0.0, | ||
), | ||
array( | ||
'isLCP' => false, | ||
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[4][self::VIDEO]', | ||
'boundingClientRect' => $test_case->get_sample_dom_rect(), | ||
'intersectionRatio' => 0.0, | ||
), | ||
), | ||
) | ||
) | ||
); | ||
} | ||
}, | ||
'buffer' => ' | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>...</title> | ||
</head> | ||
<body> | ||
<video class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="none" autoplay></video> | ||
<video class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="auto" autoplay></video> | ||
<video class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="auto" autoplay></video> | ||
<video class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="metadata" autoplay></video> | ||
<video class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" autoplay></video> | ||
</body> | ||
</html> | ||
', | ||
'expected' => ' | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>...</title> | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/poster.jpg" media="screen and (max-width: 782px)"> | ||
</head> | ||
<body> | ||
<video data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[1][self::VIDEO]" class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="none" autoplay></video> | ||
<video data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]" class="desktop" poster="https://example.com/poster.jpg" width="1200" height="500" src="https://example.com/header.mp4" preload="auto" autoplay></video> | ||
<video data-od-added-data-original-autoplay data-od-added-data-original-poster data-od-added-data-original-preload data-od-removed-autoplay data-od-removed-poster="https://example.com/poster.jpg" data-od-replaced-class="desktop" data-od-replaced-preload="auto" data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[3][self::VIDEO]" data-original-autoplay data-original-poster="https://example.com/poster.jpg" data-original-preload="auto" class="desktop wp-lazy-video" width="1200" height="500" src="https://example.com/header.mp4" preload="none" ></video> | ||
<video data-od-added-data-original-autoplay data-od-added-data-original-poster data-od-added-data-original-preload data-od-removed-autoplay data-od-removed-poster="https://example.com/poster.jpg" data-od-replaced-class="desktop" data-od-replaced-preload="metadata" data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[4][self::VIDEO]" data-original-autoplay data-original-poster="https://example.com/poster.jpg" data-original-preload="metadata" class="desktop wp-lazy-video" width="1200" height="500" src="https://example.com/header.mp4" preload="none" ></video> | ||
<video data-od-added-data-original-autoplay data-od-added-data-original-poster data-od-added-data-original-preload data-od-added-preload data-od-removed-autoplay data-od-removed-poster="https://example.com/poster.jpg" data-od-replaced-class="desktop" data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[5][self::VIDEO]" data-original-autoplay data-original-poster="https://example.com/poster.jpg" data-original-preload="default" preload="none" class="desktop wp-lazy-video" width="1200" height="500" src="https://example.com/header.mp4" ></video> | ||
<script type="module">/* const lazyVideoObserver ... */</script> | ||
<script type="module">/* import detect ... */</script> | ||
</body> | ||
</html> | ||
', | ||
); |
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.
Todo: this should happen even if it's not the LCP element but still in the viewport. But then only if that's the case across all viewports.
Something like this perhaps:
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.
The problem with
$context->url_metric_group_collection->is_every_group_populated()
is often tablets or phablets don't end up getting URL Metrics gathered for them. So I worry this would never end up returning true. Similar to #1404.So maybe this should use
$context->url_metrics_group_collection->is_any_group_populated()
as well?Maybe it should be either of those two conditions. Either:
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.
@westonruter Given that this problem is now becoming relevant in a few places and the relatively low usage of tablet devices appears to be the root of the issue, maybe we should provide an additional helper method in OD? Maybe something like
is_every_critical_group_populated()
(where only certain breakpoints would be considered critical, could be filtered)? Or, we could consider giving the breakpoints more human-friendly names likephone
,tablet
,desktop
for instance, and then have a method likeis_every_group_populated( array $groups )
where you could pass those you care about as a string?is_every_group_populated()
has the problem we're seeing here, butis_any_group_populated()
doesn't sound like an adequate solution either because why would we arbitrarily consider it fine to have either desktop or mobile data fully populated?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.
I like that idea. In #1602 I've proposed adding a
get_first_group()
andget_last_group()
toOD_URL_Metric_Group_Collection
which would return the mobile and desktop URL metric groups respectively. Based on the low usage of tablets relative to desktop and mobile, I think in reality the critical groups will always be the first and last, representing the outer bounds of the viewport widths.So with that PR, the result would be to determine whether the mobile and desktop groups are populated, this logic could be employed:
I think this could be used here instead of
$context->url_metric_group_collection->is_every_group_populated()
and$context->url_metric_group_collection->is_any_group_populated()
.Caveat: It could be that for a particular site the traffic is almost exclusively coming by tablet visitors. If this is the case, a site owner could indicate this by making it so that there is only one group:
In this case the first and last group are the same group.