From 39f797e1f69f2832e5a8ffd297f3e02163aea2b8 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 9 Apr 2026 11:06:54 -0600 Subject: [PATCH 1/4] Add native loading=lazy support for video elements Add loading="lazy" as a progressive enhancement for video elements that are below the fold, alongside the existing JavaScript-based lazy loading. For LCP and visible videos, remove any existing loading attribute to prevent unintended lazy loading. This leverages the new HTML spec support for loading="lazy" on video elements (whatwg/html#10376) while keeping the JS IntersectionObserver fallback for browsers that don't yet support it. --- ...ss-image-prioritizer-video-tag-visitor.php | 11 +++++++ .../expected.html | 6 ++-- .../buffer.html | 13 ++++++++ .../expected.html | 16 ++++++++++ .../set-up.php | 30 +++++++++++++++++++ .../buffer.html | 13 ++++++++ .../expected.html | 15 ++++++++++ .../set-up.php | 30 +++++++++++++++++++ .../buffer.html | 13 ++++++++ .../expected.html | 16 ++++++++++ .../set-up.php | 30 +++++++++++++++++++ 11 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/buffer.html create mode 100644 plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/expected.html create mode 100644 plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/set-up.php create mode 100644 plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/buffer.html create mode 100644 plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/expected.html create mode 100644 plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/set-up.php create mode 100644 plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/buffer.html create mode 100644 plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/expected.html create mode 100644 plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/set-up.php diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php index 2bd0726468..b41fadf91d 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -216,14 +216,25 @@ private function lazy_load_videos( ?string $poster, OD_Tag_Visitor_Context $cont if ( 'auto' !== $initial_preload ) { $processor->set_attribute( 'preload', 'auto' ); } + if ( null !== $processor->get_attribute( 'loading' ) ) { + $processor->remove_attribute( 'loading' ); + } return; } // If the element is visible in any viewport, do not lazy-load it. if ( $max_intersection_ratio > 0 ) { + if ( null !== $processor->get_attribute( 'loading' ) ) { + $processor->remove_attribute( 'loading' ); + } return; } + // Add native lazy loading for browsers that support it. + if ( 'lazy' !== $this->get_attribute_value( $processor, 'loading' ) ) { + $processor->set_attribute( 'loading', 'lazy' ); + } + if ( 'none' !== $initial_preload ) { $processor->set_attribute( 'data-original-preload', null !== $initial_preload ? $initial_preload : 'default' ); $processor->set_attribute( 'preload', 'none' ); diff --git a/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html b/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html index 7a1458bba1..5ac2b6df42 100644 --- a/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html @@ -10,9 +10,9 @@
- - - + + +
diff --git a/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/buffer.html b/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/buffer.html new file mode 100644 index 0000000000..991f9d0477 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/buffer.html @@ -0,0 +1,13 @@ + + + + ... + + + + +
+ +
+ + diff --git a/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/expected.html b/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/expected.html new file mode 100644 index 0000000000..9779e9fce4 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/expected.html @@ -0,0 +1,16 @@ + + + + ... + + + + +
+ +
+ + + + + diff --git a/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/set-up.php b/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/set-up.php new file mode 100644 index 0000000000..e785d410ce --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/set-up.php @@ -0,0 +1,30 @@ +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' => false, + 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[1][self::VIDEO]', + 'boundingClientRect' => $test_case->get_sample_dom_rect(), + 'intersectionRatio' => 0.0, + ), + ), + ) + ) + ); + } +}; diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/buffer.html b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/buffer.html new file mode 100644 index 0000000000..586a78c916 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/buffer.html @@ -0,0 +1,13 @@ + + + + ... + + + + +
+ +
+ + diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/expected.html b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/expected.html new file mode 100644 index 0000000000..2dbf76fce8 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/expected.html @@ -0,0 +1,15 @@ + + + + ... + + + + +
+ +
+ + + + diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/set-up.php b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/set-up.php new file mode 100644 index 0000000000..7eb4dae9b4 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-already-set-and-is-visible/set-up.php @@ -0,0 +1,30 @@ +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' => false, + 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[1][self::VIDEO]', + 'boundingClientRect' => $test_case->get_sample_dom_rect(), + 'intersectionRatio' => 0.5, + ), + ), + ) + ) + ); + } +}; diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/buffer.html b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/buffer.html new file mode 100644 index 0000000000..586a78c916 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/buffer.html @@ -0,0 +1,13 @@ + + + + ... + + + + +
+ +
+ + diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/expected.html b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/expected.html new file mode 100644 index 0000000000..2950908bb9 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/expected.html @@ -0,0 +1,16 @@ + + + + ... + + + + + +
+ +
+ + + + diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/set-up.php b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/set-up.php new file mode 100644 index 0000000000..ffcf87e56f --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-with-loading-lazy-on-lcp-element/set-up.php @@ -0,0 +1,30 @@ +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' => '/HTML/BODY/DIV[@id=\'page\']/*[1][self::VIDEO]', + 'boundingClientRect' => $test_case->get_sample_dom_rect(), + 'intersectionRatio' => 1.0, + ), + ), + ) + ) + ); + } +}; From 771be4f4e64fa96848ef2d7ada5f9842ba7bf2df Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Mon, 11 May 2026 08:50:50 -0700 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Weston Ruter --- .../class-image-prioritizer-video-tag-visitor.php | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php index b41fadf91d..54dab6006d 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -216,24 +216,18 @@ private function lazy_load_videos( ?string $poster, OD_Tag_Visitor_Context $cont if ( 'auto' !== $initial_preload ) { $processor->set_attribute( 'preload', 'auto' ); } - if ( null !== $processor->get_attribute( 'loading' ) ) { - $processor->remove_attribute( 'loading' ); - } + $processor->remove_attribute( 'loading' ); return; } // If the element is visible in any viewport, do not lazy-load it. if ( $max_intersection_ratio > 0 ) { - if ( null !== $processor->get_attribute( 'loading' ) ) { - $processor->remove_attribute( 'loading' ); - } + $processor->remove_attribute( 'loading' ); return; } // Add native lazy loading for browsers that support it. - if ( 'lazy' !== $this->get_attribute_value( $processor, 'loading' ) ) { - $processor->set_attribute( 'loading', 'lazy' ); - } + $processor->set_attribute( 'loading', 'lazy' ); if ( 'none' !== $initial_preload ) { $processor->set_attribute( 'data-original-preload', null !== $initial_preload ? $initial_preload : 'default' ); From e871d03c80211aae24c28b3c4a6d9a150bd4b223 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Mon, 11 May 2026 08:55:32 -0700 Subject: [PATCH 3/4] Short-circuit lazy video IntersectionObserver when native lazy-loading is supported When the browser supports `loading="lazy"` on video elements (detected via `'loading' in HTMLMediaElement.prototype`), restore the original preload, autoplay, and poster attributes immediately. The browser then defers the video load itself via the `loading="lazy"` attribute, avoiding the need for an IntersectionObserver-based fallback. Addresses review feedback on PR #2450. --- plugins/image-prioritizer/lazy-load-video.js | 72 ++++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/plugins/image-prioritizer/lazy-load-video.js b/plugins/image-prioritizer/lazy-load-video.js index c569fa42be..59b6f5e2f2 100644 --- a/plugins/image-prioritizer/lazy-load-video.js +++ b/plugins/image-prioritizer/lazy-load-video.js @@ -1,38 +1,50 @@ -const lazyVideoObserver = new IntersectionObserver( - ( entries ) => { - for ( const entry of entries ) { - if ( entry.isIntersecting ) { - const video = /** @type {HTMLVideoElement} */ entry.target; +const restoreVideo = ( /** @type {HTMLVideoElement} */ video ) => { + const poster = video.getAttribute( 'data-original-poster' ); + if ( poster ) { + video.setAttribute( 'poster', poster ); + } - const poster = video.getAttribute( 'data-original-poster' ); - if ( poster ) { - video.setAttribute( 'poster', poster ); - } + if ( video.hasAttribute( 'data-original-autoplay' ) ) { + video.setAttribute( 'autoplay', 'autoplay' ); + } - if ( video.hasAttribute( 'data-original-autoplay' ) ) { - video.setAttribute( 'autoplay', 'autoplay' ); - } + const preload = video.getAttribute( 'data-original-preload' ); + if ( preload ) { + if ( 'default' === preload ) { + video.removeAttribute( 'preload' ); + } else { + video.setAttribute( 'preload', preload ); + } + } +}; - const preload = video.getAttribute( 'data-original-preload' ); - if ( preload ) { - if ( 'default' === preload ) { - video.removeAttribute( 'preload' ); - } else { - video.setAttribute( 'preload', preload ); - } - } +const videos = document.querySelectorAll( 'video.od-lazy-video' ); - lazyVideoObserver.unobserve( video ); +// When the browser natively supports lazy-loading on video, restore the original +// attributes immediately and rely on loading="lazy" to defer the video load. +if ( 'loading' in HTMLMediaElement.prototype ) { + for ( const video of videos ) { + restoreVideo( /** @type {HTMLVideoElement} */ ( video ) ); + } +} else { + const lazyVideoObserver = new IntersectionObserver( + ( entries ) => { + for ( const entry of entries ) { + if ( entry.isIntersecting ) { + restoreVideo( + /** @type {HTMLVideoElement} */ ( entry.target ) + ); + lazyVideoObserver.unobserve( entry.target ); + } } + }, + { + rootMargin: '100% 0% 100% 0%', + threshold: 0, } - }, - { - rootMargin: '100% 0% 100% 0%', - threshold: 0, - } -); + ); -const videos = document.querySelectorAll( 'video.od-lazy-video' ); -for ( const video of videos ) { - lazyVideoObserver.observe( video ); + for ( const video of videos ) { + lazyVideoObserver.observe( video ); + } } From d8e679456a673de7e1cebf117a49505d8822417c Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Mon, 11 May 2026 09:03:10 -0700 Subject: [PATCH 4/4] Update test snapshots to reflect renamed const in lazy-load-video.js The test helper normalizes inline scripts by replacing them with a `/* const ... */` placeholder taken from the first declared identifier. After refactoring lazy-load-video.js to begin with `const restoreVideo`, the expected snapshots need to match. --- .../test-cases/multiple-videos-on-all-breakpoints/expected.html | 2 +- .../test-cases/video-hidden-on-all-breakpoints/expected.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html b/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html index 5ac2b6df42..9c09c446e5 100644 --- a/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html @@ -14,7 +14,7 @@ - + diff --git a/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/expected.html b/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/expected.html index 9779e9fce4..526abbd89f 100644 --- a/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/video-hidden-on-all-breakpoints/expected.html @@ -9,7 +9,7 @@
- +