Skip to content

Commit

Permalink
Merge branch 'trunk' into feature/delete-original-option-modern-image…
Browse files Browse the repository at this point in the history
…-formats
  • Loading branch information
AhmarZaidi committed Oct 18, 2024
2 parents 8b7f4d7 + 70cd02b commit 6499e55
Show file tree
Hide file tree
Showing 13 changed files with 389 additions and 47 deletions.
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'
* @phpstan-type NormalizedAttributeNames 'fetchpriority'|'loading'|'crossorigin'|'preload'
*
* @since 0.1.0
* @access private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,28 @@
*/
final class Image_Prioritizer_Video_Tag_Visitor extends Image_Prioritizer_Tag_Visitor {

/**
* Class name used to indicate a video which is lazy-loaded.
*
* @since n.e.x.t
* @var string
*/
const LAZY_VIDEO_CLASS_NAME = 'od-lazy-video';

/**
* Whether the lazy-loading script was added to the body.
*
* @since n.e.x.t
* @var bool
*/
protected $added_lazy_script = false;

/**
* Visits a tag.
*
* @param OD_Tag_Visitor_Context $context Tag visitor context.
* @since n.e.x.t
*
* @param OD_Tag_Visitor_Context $context Tag visitor context.
* @return bool Whether the tag should be tracked in URL metrics.
*/
public function __invoke( OD_Tag_Visitor_Context $context ): bool {
Expand All @@ -35,18 +52,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;
}

/**
Expand Down Expand Up @@ -123,7 +138,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',
Expand All @@ -145,4 +160,90 @@ private function preload_poster_image( string $poster, OD_Tag_Visitor_Context $c
);
}
}

/**
* Optimizes the VIDEO tag based on whether it is the LCP element or else whether it is displayed in any 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;

/*
* Do not do any lazy-loading if the mobile and desktop viewport groups lack URL metrics. This is important
* because if there is a VIDEO in the initial viewport on desktop but not mobile, if then there are only URL
* metrics collected for mobile then the VIDEO will get lazy-loaded which is good for mobile but for desktop
* it will hurt performance. So this is why it is important to have URL metrics collected for both desktop and
* mobile to verify whether maximum intersectionRatio is accounting for both screen sizes.
* TODO: Add this same condition to IMG lazy-loading and Embed lazy-loading.
*/
if (
$context->url_metric_group_collection->get_first_group()->count() === 0
||
$context->url_metric_group_collection->get_last_group()->count() === 0
) {
return;
}

$xpath = $processor->get_xpath();

$initial_preload = $this->get_attribute_value( $processor, 'preload' );
$max_intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( $xpath );

/*
* Optimize the video preload value based on how visible the video is in the viewport. If it is the common LCP
* element, then make sure that preload is set to auto so that the browser is encouraged to download more of the
* video. Nevertheless, it is likely that get_common_lcp_element() may return null since URL Metrics for
* phablets and tablets may not get frequently gathered.
*
* So, for figure consideration this should perhaps check if the element in all gathered URL Metrics for all
* viewport groups has an intersectionRatio greater than zero. If so, then it can also set it to preload=auto.
* Otherwise, if the maximum intersectionRatio is greater than zero and yet there are also instances of the
* element in one or more URL Metrics for which the intersectionRatio is zero, then in this case it may make
* sense to set preload to metadata. This would avoid potentially aggressive downloading of a video when it is
* not visible on mobile and yet it is visible on desktop. For prioritizing the loading of image which is the
* LCP element for one viewport but not another the solution is to use a preload link with a media query.
* However, videos are not supported in preload links, so we have to resort to trying pick the best preload
* value attribute to be shared across all viewports.
* TODO: The above paragraph.
*/
$common_lcp_element = $context->url_metric_group_collection->get_common_lcp_element();
if ( null !== $common_lcp_element && $xpath === $common_lcp_element['xpath'] ) {
if ( 'auto' !== $initial_preload ) {
$processor->set_attribute( 'preload', 'auto' );
}
return;
}

// If the element is visible in any viewport, do not lazy-load it.
if ( $max_intersection_ratio > 0 ) {
return;
}

if ( 'none' !== $initial_preload ) {
$processor->set_attribute( 'data-original-preload', null !== $initial_preload ? $initial_preload : 'default' );
$processor->set_attribute( 'preload', 'none' );
$processor->add_class( self::LAZY_VIDEO_CLASS_NAME );
}

if ( null !== $processor->get_attribute( 'autoplay' ) ) {
$processor->set_attribute( 'data-original-autoplay', true );
$processor->remove_attribute( 'autoplay' );
$processor->add_class( self::LAZY_VIDEO_CLASS_NAME );
}

if ( null !== $poster ) {
$processor->set_attribute( 'data-original-poster', $poster );
$processor->remove_attribute( 'poster' );
$processor->add_class( self::LAZY_VIDEO_CLASS_NAME );
}

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;
}
}
}
19 changes: 19 additions & 0 deletions plugins/image-prioritizer/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,22 @@
}

add_action( 'od_init', 'image_prioritizer_init' );

/**
* Gets the script to lazy-load videos.
*
* Load a video and its poster image when it approaches the viewport using an IntersectionObserver.
*
* Handles 'autoplay' and 'preload' attributes accordingly.
*
* @since n.e.x.t
*/
function image_prioritizer_get_lazy_load_script(): string {
$script = file_get_contents( __DIR__ . '/lazy-load.js' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request.

if ( false === $script ) {
return '';
}

return $script;
}
42 changes: 42 additions & 0 deletions plugins/image-prioritizer/lazy-load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const lazyVideoObserver = new IntersectionObserver(
( entries ) => {
for ( const entry of entries ) {
if ( entry.isIntersecting ) {
const video = /** @type {HTMLVideoElement} */ entry.target;

if ( video.hasAttribute( 'data-original-poster' ) ) {
video.setAttribute(
'poster',
video.getAttribute( 'data-original-poster' )
);
}

if ( video.hasAttribute( 'data-original-autoplay' ) ) {
video.setAttribute( 'autoplay', 'autoplay' );
}

if ( video.hasAttribute( 'data-original-preload' ) ) {
const preload = video.getAttribute(
'data-original-preload'
);
if ( 'default' === preload ) {
video.removeAttribute( 'preload' );
} else {
video.setAttribute( 'preload', preload );
}
}

lazyVideoObserver.unobserve( video );
}
}
},
{
rootMargin: '100% 0% 100% 0%',
threshold: 0,
}
);

const videos = document.querySelectorAll( 'video.od-lazy-video' );
for ( const video of videos ) {
lazyVideoObserver.observe( video );
}
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/poster1.jpg" width="1200" height="500" src="https://example.com/header1.mp4" preload="none" autoplay></video>
<video class="desktop" poster="https://example.com/poster2.jpg" width="1200" height="500" src="https://example.com/header2.mp4" preload="auto" autoplay></video>
<video class="desktop" poster="https://example.com/poster3.jpg" width="1200" height="500" src="https://example.com/header3.mp4" preload autoplay></video>
<video class="desktop" poster="https://example.com/poster4.jpg" width="1200" height="500" src="https://example.com/header4.mp4" preload="metadata" autoplay></video>
<video class="desktop" poster="https://example.com/poster5.jpg" width="1200" height="500" src="https://example.com/header5.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/poster1.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/poster1.jpg" width="1200" height="500" src="https://example.com/header1.mp4" preload="auto" autoplay></video>
<video data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]" class="desktop" poster="https://example.com/poster2.jpg" width="1200" height="500" src="https://example.com/header2.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/poster3.jpg" data-od-replaced-class="desktop" data-od-replaced-preload data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[3][self::VIDEO]" data-original-autoplay data-original-poster="https://example.com/poster3.jpg" data-original-preload class="desktop od-lazy-video" width="1200" height="500" src="https://example.com/header3.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/poster4.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/poster4.jpg" data-original-preload="metadata" class="desktop od-lazy-video" width="1200" height="500" src="https://example.com/header4.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/poster5.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/poster5.jpg" data-original-preload="default" preload="none" class="desktop od-lazy-video" width="1200" height="500" src="https://example.com/header5.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,82 @@
<?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/poster1.jpg" width="1200" height="500" src="https://example.com/header1.mp4" preload="none" autoplay></video>
<video class="desktop" poster="https://example.com/poster2.jpg" width="1200" height="500" src="https://example.com/header2.mp4" preload="auto" autoplay></video>
<video class="desktop" poster="https://example.com/poster3.jpg" width="1200" height="500" src="https://example.com/header3.mp4" preload="auto" autoplay></video>
<video class="desktop" poster="https://example.com/poster4.jpg" width="1200" height="500" src="https://example.com/header4.mp4" preload="metadata" autoplay></video>
<video class="desktop" poster="https://example.com/poster5.jpg" width="1200" height="500" src="https://example.com/header5.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/poster1.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/poster1.jpg" width="1200" height="500" src="https://example.com/header1.mp4" preload="none" autoplay></video>
<video data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]" class="desktop" poster="https://example.com/poster2.jpg" width="1200" height="500" src="https://example.com/header2.mp4" preload="auto" autoplay></video>
<video data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[3][self::VIDEO]" class="desktop" poster="https://example.com/poster3.jpg" width="1200" height="500" src="https://example.com/header3.mp4" preload="auto" autoplay></video>
<video data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[4][self::VIDEO]" class="desktop" poster="https://example.com/poster4.jpg" width="1200" height="500" src="https://example.com/header4.mp4" preload="metadata" autoplay></video>
<video data-od-xpath="/*[1][self::HTML]/*[2][self::BODY]/*[5][self::VIDEO]" class="desktop" poster="https://example.com/poster5.jpg" width="1200" height="500" src="https://example.com/header5.mp4" autoplay></video>
<script type="module">/* import detect ... */</script>
</body>
</html>
',
);
Loading

0 comments on commit 6499e55

Please sign in to comment.