Skip to content

Add native loading=lazy support for video elements#2450

Open
adamsilverstein wants to merge 5 commits into
trunkfrom
add-native-video-support
Open

Add native loading=lazy support for video elements#2450
adamsilverstein wants to merge 5 commits into
trunkfrom
add-native-video-support

Conversation

@adamsilverstein
Copy link
Copy Markdown
Member

@adamsilverstein adamsilverstein commented Apr 9, 2026

Summary

Closes #2400.

  • Adds loading="lazy" as a progressive enhancement for <video> elements that are below the fold (intersectionRatio = 0), alongside the existing JavaScript IntersectionObserver-based lazy loading
  • Removes any existing loading attribute from LCP videos and visible videos to prevent unintended lazy loading
  • Keeps the JS fallback (od-lazy-video class, data-original-* attributes, lazy-load-video.js) since browser support for loading="lazy" on video is still rolling out

This leverages the new HTML spec support for loading="lazy" on video elements (whatwg/html#10376). In unsupported browsers, the attribute is simply ignored (zero cost). Audio tag support is deferred to a separate PR.

Test plan

  • Verify existing snapshot tests pass with updated expected outputs
  • New test: hidden video gets loading="lazy" alongside JS lazy loading
  • New test: visible video with loading="lazy" has the attribute removed
  • New test: LCP video with loading="lazy" has the attribute removed
  • PHPStan passes at level 10

AI Usage

I used Claude code to write the code and tests for this PR which I reviewed. It should probably be manually tested as well, I have not done that yet. The PR is mostly test cases, the code change itself is minor.

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.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.34%. Comparing base (097e644) to head (d8e6794).
⚠️ Report is 21 commits behind head on trunk.

Additional details and impacted files
@@            Coverage Diff             @@
##            trunk    #2450      +/-   ##
==========================================
+ Coverage   69.17%   69.34%   +0.17%     
==========================================
  Files          90       90              
  Lines        8243     7752     -491     
==========================================
- Hits         5702     5376     -326     
+ Misses       2541     2376     -165     
Flag Coverage Δ
multisite 69.34% <100.00%> (+0.17%) ⬆️
single 35.71% <0.00%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@adamsilverstein adamsilverstein added the [Type] Feature A new feature within an existing module label Apr 10, 2026
@adamsilverstein adamsilverstein marked this pull request as ready for review April 10, 2026 21:10
@github-actions
Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>
Co-authored-by: westonruter <westonruter@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@adamsilverstein adamsilverstein self-assigned this Apr 10, 2026
@adamsilverstein adamsilverstein added [Plugin] Enhanced Responsive Images Issues for the Enhanced Responsive Images plugin (formerly Auto Sizes) [Plugin] Image Prioritizer Issues for the Image Prioritizer plugin (dependent on Optimization Detective) and removed [Plugin] Enhanced Responsive Images Issues for the Enhanced Responsive Images plugin (formerly Auto Sizes) labels Apr 10, 2026
@adamsilverstein
Copy link
Copy Markdown
Member Author

I verified this worked as expected on a test page in my local. Non LCP video tags get the lazy loading attribute:

<figure class="wp-block-video"><video class="od-lazy-video" data-od-added-class data-od-added-data-original-preload data-od-added-loading data-od-added-preload data-od-xpath="/HTML/BODY/DIV[@id=&apos;page&apos;]/*[3][self::DIV]/*[1][self::DIV]/*[1][self::MAIN]/*[1][self::ARTICLE]/*[1][self::DIV]/*[12][self::FIGURE]/*[1][self::VIDEO]" data-original-preload="default" loading="lazy" preload="none" height="904" style="aspect-ratio: 1392 / 904;" width="1392" controls src="https://wpdev.localhost/wp-content/uploads/2026/04/inner-button-turns-to-save.mp4"></video></figure>

Comment thread plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php Outdated
Comment thread plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php Outdated
Comment thread plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php Outdated
@westonruter
Copy link
Copy Markdown
Member

  • Keeps the JS fallback (od-lazy-video class, data-original-* attributes, lazy-load-video.js) since browser support for loading="lazy" on video is still rolling out

Should this check for native support for video lazy-loading and short-circuit when this is the case? The logic in the JS can short-circuit if 'loading' in document.createElement('video').

@westonruter
Copy link
Copy Markdown
Member

Should this check for native support for video lazy-loading and short-circuit when this is the case? The logic in the JS can short-circuit if 'loading' in document.createElement('video').

Actually, this is probably the better check:

'loading' in HTMLMediaElement.prototype

I just updated to Chrome 148 and confirm that this is true.

adamsilverstein and others added 2 commits May 11, 2026 08:50
Co-authored-by: Weston Ruter <westonruter@gmail.com>
…g 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.
@adamsilverstein
Copy link
Copy Markdown
Member Author

Updated in e871d03 to short-circuit the IntersectionObserver when 'loading' in HTMLMediaElement.prototype. When native video lazy-loading is supported, the original preload, autoplay, and poster attributes are restored immediately and the browser handles the deferral via loading="lazy". Otherwise, the IntersectionObserver fallback runs as before.

The three inline suggestions from your review are already applied in 771be4f.

The test helper normalizes inline scripts by replacing them with a
`/* const <varname> ... */` placeholder taken from the first declared
identifier. After refactoring lazy-load-video.js to begin with
`const restoreVideo`, the expected snapshots need to match.
Comment on lines +30 to +43
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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something that just came to mind: What about sites that are configured with infinite scroll? The newly-inserted content could come in with the autoplay and preload attributes removed.

Perhaps this is unlikely because it would mean Image Prioritizer would have to be running on the Ajax response.

Also, worst case is it means the video wouldn't get preloaded, meaning the user would have to click it to play. Not the end of the world. Since it is implemented in Chrome now, appears to be implemented behind a flag in Firefox and the Safari implementation is also moving along, I expect we can soon eliminate the JS here entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Plugin] Image Prioritizer Issues for the Image Prioritizer plugin (dependent on Optimization Detective) [Type] Feature A new feature within an existing module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add native lazy loading support for HTML video and audio tags

2 participants