WordPress.org

Make WordPress Core

Opened 14 months ago

Last modified 21 hours ago

#44427 new feature request

Introduce lazy-loading API for media and other elements

Reported by: mor10 Owned by:
Milestone: Awaiting Review Priority: normal
Severity: normal Version:
Component: Media Keywords: needs-unit-tests has-patch
Focuses: performance Cc:

Description

Synopsis

Lazy-loading of media and other elements (iframes, embeds, etc) is a performance best practice. Currently, various plugins including Jetpack, and themes including WP Rig ship lazy loading features to improve performance. These solutions rely on conditionally rewriting the HTML for images and other media as it is being loaded from core which is sub-optimal.

Proposal

Introduce a lazy-loading API in core which lazy-loads media and other elements out of the box based on various conditions, with an opt-out feature in the Customizer. This takes the onus of having extensive knowledge of performance optimization and how to add lazy-loading to your site off the shoulders of the end-user and gives developers control over how and when content is lazy-loaded.

The lazy-loading API should allow theme- and plugin developers detailed control over how lazy-loading works. As an example, the AMP plugin would disable core lazy-loading and replace it with its own custom method. Another example: A theme developer may choose to immediately load the featured image of the first post in an index or archive template while all other images are lazy-loaded.

Why this isn't plugin territory

Lazy-loading is currently considered plugin territory. I disagree for a couple of reasons:

  1. Lazy-loading is performance best-practice: https://developers.google.com/web/fundamentals/performance/lazy-loading-guidance/images-and-video/.
  2. WordPress should not put the onus on site-owners to have in-depth knowledge of performance best-practices and how to meet them.
  3. With no consistent API, plugins and themes may introduce different and conflicting solutions to the same problem which in turn causes unexpected behavior for the end-user.
  4. Lazy-loading introduced at plugin/theme level forces aggressive rewriting of content which would be more consistently performed at core.

Attachments (3)

lazy-load-theme-screenshots.patch (4.6 KB) - added by pierlo 5 days ago.
44427.diff (2.6 KB) - added by spacedmonkey 41 hours ago.
44427.2.diff (3.2 KB) - added by spacedmonkey 41 hours ago.

Download all attachments as: .zip

Change History (39)

#1 @tabrisrp
14 months ago

As the developer of a performance optimization plugin, I definitely relate with points 3 and 4, which are recurring issues for both our development & support teams.

Having a standardized lazy-loading API in core would greatly reduce the conflicts we encounter, and reduce our need to consistently & accurately parse the HTML content of pages to detect and rewrite lazy-loadable media assets.

I believe it would also help solve difficult situations like lazy-loading background images, a common user-requested feature, which is hard to get around without a core standard.

#2 follow-up: @jonoaldersonwp
14 months ago

This feels like a great win for performance, standards, carbon, etc. The SEO and performance nerd in me loves this.

Is there any reason why we can't/shouldn't implement this, verbatim, as per the Google documentation (using the event handlers method, until such time as the intersection observer method is universally supported)?

Given how minimal the JS size and processing overhead is, and given that we'll want lazy-loading to be default on (at least, in future/ongoing theme dev etc), I've no objections to the JS being autoloaded on all front-end requests (unless it's specifically disabled/de-enqueued).

I do think that there may be some nuance in implementation; some scenarios:

  1. Inline images, inserted in content; where we'll be adding a bunch of data- params to the code. We'll need any existing or new filtering/validation to anticipate and manage this (or, to transform the code via a filter on the_content, if we're concerned about code bloat or confusion in the content editor).

1a) Gutenberg blocks.

1b) images in non-webpage context / where js isn't available, like REST or RSS.

  1. Theme/template images which use wp_get_attachment_image() or similar to output image HTML. We'll need to hook into these with an additional parameter (we shouldn't make this a universal default/assumption, as many themes use 'hero images' in header areas where lazy-loading may produce an unwanted delay).
  1. Theme/template images which use wp_get_attachment_image_srcset() or similar to construct the output of the img/picture etc tag. This poses a challenge.

The first and the second feel achievable; some settings, an extra parameter in the case of 2, and everything else can happen under the hood.

The third feels like the messiest for easy adoption; we're relying on a significant change to coding approaches, which @mor10 rightly wants to avoid. Thoughts appreciated!

Last edited 14 months ago by jonoaldersonwp (previous) (diff)

#3 in reply to: ↑ 2 @flixos90
13 months ago

Is there any reason why we can't/shouldn't implement this, verbatim, as per the Google documentation (using the event handlers method, until such time as the intersection observer method is universally supported)?

I like this idea, relying on a method that has been recommended by one of the expert sources in the field seems like a solid approach to me. We might even implement both ways in one script, and only execute the modern one if "IntersectionObserver" in window. Otherwise use the event listener one.

Before we dive into implementation further, I think we need to discuss how this should integrate with themes.

  • Given the need for some images to always load immediately and skip lazy-load, I suggest we only lazy load when the theme does add_theme_support( 'lazy-load' ). While this will result in adoption of lazy-loading not drastically increasing immediately, experience has proven that, over time, most theme features introduced were at a certain point commonly supported by most themes (think about title-tag for example). We just need to document well, make supporting the feature easy and encourage it. I think by making it opt-in, we're on the safe side. Also, some themes include this already, and there are plugins implementing that, so with opt-in we ensure that there are no conflicts for those sites.
  • I think we should only account for what we can reasonably account for. <img> tags in filterable content should be adjusted, as well as any calls to wp_get_attachment_image(). However, when people use other techniques (i.e. "hard-code" the <img> tags themselves, what you @jonoaldersonwp mentioned too), we shouldn't automatically touch this, as it would require some output buffering, which we should avoid IMO. Documentation should rather highlight that this way of including images is not recommended and wp_get_attachment_image() should be used instead. Something I'm not sure about is how to deal with background images as there's no standardized way of having that markup generated (also it's only markup if through an inline style attribute, otherwise in a CSS file, which makes this more tricky).
  • We need to figure out a way to easily allow theme developers to indicate that specific images must not be lazy-loaded. This could mean having a specific CSS class for this, or such thing as a data-skip-lazy-load attribute. Possibly, we could allow content creators to handle this as well, by putting a checkbox into the "Insert Media" modal somewhere, which says "Skip lazy-loading for this image?" - and if so, put the class or attribute into the generated tag.

#4 @jonoaldersonwp
13 months ago

  • Agreed on requiring explicit theme support.
  • Agreed on only catching common/reasonable use-cases (e.g., <img> tags and get_attachment_ derivatives)
  • Suggest we ignore background images (for now), as this adds a ton of additional complexity and overhead.
  • Agree on data- attributes (and/or a class name) for skipping lazy load, and for a checkbox against the media entity.

So, specifics:

  • We need to agree on a theme support hook. I propose lazy-load-images for some additional specificity, as we might also do something with lazy-load-{{other}} in the future.
  • We need a checkbox against all media where the mime type is of an image format and the wording around that. E.g., "Disable lazy-loading for this image".
  • We need to agree a data- attribute or class name for skipping lazy load. Agreed on data-skip-lazy-load. Whilst I'd prefer an attribute, my gut feeling is that might be a little foreign to most content editors and some theme developers; whereas a class (skip-lazy-load) might be a more recognisable approach. No reason we can't support both.
  • We need to identify the get_attachment_ functions which return image HTML (as opposed to attributes) and assess the impact of filtering the output of these.
  • Update relevant documentation.
  • Build the thing.

#5 @tabrisrp
13 months ago

Here is a list of functions we identified, which outputs HTML for images and have an available filter:

get_the_post_thumbnail() uses wp_get_attachment_image() under the hood, which doesn't have a filter for the whole HTML returned (see https://core.trac.wordpress.org/ticket/27399), only for the attributes with wp_get_attachment_image_attributes.

In the case of images added into the main content and in widgets like the image widget, I suppose the best approach would be to add the attributes directly to the generated HTML at that time, the same way than for the responsive attributes.

#6 @jonoaldersonwp
13 months ago

Ah, yes, maybe we should have a data-enable-lazy-load attribute, too, for cases where an image might otherwise have been passively excluded, but the theme/content author wants it to be explicitly included.

Last edited 13 months ago by jonoaldersonwp (previous) (diff)

#7 @tabrisrp
9 months ago

How can we help this move forward?

#8 follow-up: @mor10
4 months ago

Lazy loading is becoming a web standard, with implementation in Chrome just around the corner. This should be the model anything with lazy loading is based on in WordPress core:

https://addyosmani.com/blog/lazy-loading/

https://groups.google.com/a/chromium.org/forum/m/#!topic/blink-dev/jxiJvQc-gVg

This is non-destructive and backwards compatible. Shipping it in WP core will lay down the necessary cowpath for widespread browser support. I suggest we work with the team behind this feature proposal to make a best-practice implementation.

Some suggestions:

  • all images lazy by default.
  • first featured image on singular view eager by default.
  • API for theme / plugin developers to control loading attribute on case-by-case basis.
  • optional: core to ship JS shim for backwards compat.

#9 in reply to: ↑ 8 @jonoaldersonwp
4 months ago

Agreed.

#10 @tabrisrp
4 months ago

Agreed, and it also simplifies the implementation as no additional Javascript would be required by default. If adding a shim, it should only support IntersectionObserver now, as it's supported in all modern browsers now (Safari 12.2 was the last to adopt it).

This also removes the need to brainstorm on data attributes to use to allow/disable lazyload.

Adding support via add_theme_support( 'lazy-load-images' ); and add_theme_support( 'lazy-load-iframes' ); will allow themes and plugins developers to provide a specific value for any image/iframe handled.

Examples: adding a loading="eager" value for the logo in the header for a theme or the first slide of a slider created with a slider plugin, while the rest of the slides have the loading="lazy" value.

Last edited 4 months ago by tabrisrp (previous) (diff)

#11 @spacedmonkey
3 months ago

  • Focuses performance added
  • Keywords needs-patch needs-unit-tests added

I have create an issue on the github repo for gutenberg for this.

I think we can get this in WP 5.3.

Developers can start playing with this attribute with a filter as simple as this.

add_filter( 'wp_get_attachment_image_attributes', function( array $attr ) {
        $attr['loading'] = 'lazy';
        return $attr;
});

#12 @pcfreak30
3 months ago

While I like this idea, I think it should be pointed out that we should not re-invent the wheel with a lazyload library. Not sure if that was anyone thought process, but seemed like it. One that I have seen google link to (forgot where) and as a link on pagespeed I think, is https://github.com/aFarkas/lazysizes.

They also have an IntersectionObserver version at https://github.com/aFarkas/lazysizes/blob/gh-pages/src/lazysizes-intersection.js which uses no scroll timers. The code uses no jquery and also makes changes with requestAnimationFrame with a simplistic queue system for performance vs setTimeout. Among it is a folder with like 20 plugins, thus can be used for custom ones.

Thanks.

#13 @mor10
12 days ago

Native lazy-loading is now shipping in the latest version of Chrome. This is a live feature on the web with graceful degradation built in. Implementing it in WP Core is non-destructive and straight forward. Time to move!

https://web.dev/native-lazy-loading

#14 @jonoaldersonwp
11 days ago

Yep! How? :P

#15 @JakePT
11 days ago

In all these discussions of lazy loading I don't see anyone considering that lazy loading on a slow connection makes for an absolutely abysmal browsing experience. Has no one browsed the web on a slow connection before? If I'm loading a long-ish article with images on a slow connection, then I want those images to start loading immediately so that they're visible by the time that I get to them. If lazy loading is enabled, or starts loading images too late, then the user is forced to stop and wait for the image to load each time they reach one. It's the equivalent of these web video players that only buffer a small amount of the video. When I had a poor connection in the past there were sites whose videos I could not watch at all (ever) because it was impossible to let the video load. This is a similar issue. It saves the site owner bandwidth, but makes browsing/watching utterly miserable.

I'd also like somebody to actually articulate what the performance benefits for lazy loading actually are before anything is done. This ticket and the Google article go on about performance, performance, performance, but with no real data. Obviously without lazy loading the page isn't considered fully loaded until all the images are loaded, which means that a page with lazy loading would be considered "fully loaded" sooner, but is that meaningful? One could argue that by that metric a page with lazy loaded images isn't truly fully loaded until I scroll to the bottom to cause all the images to load. Does the browser loading images below the fold actually significantly affect time to interactive? Does that outweigh the user experience costs I've described? We're seeing browsers and libraries specifically designed to pre-load entire webpages in the background so that when the user clicks a link they perceive the next page to have loaded instantly, but we're simultaneously seeing efforts to prevent content on the same page from being pre-loaded, also in the name of performance. How does that make sense?

Google themselves say:

Today, Chrome already loads images at different priorities depending on where they're located with respect to the device viewport. Images below the viewport are loaded with a lower priority, but they're still fetched as soon as possible.

Is that not already sufficient for perceived performance? How does lazy loading improve performance over that?

So my argument is that while lazy loading might help with bandwidth and memory usage, which is not meaningless, it is a considerably poorer user experience for anyone that is not on a high speed connection. As such I believe that any application of lazy loading needs to carefully consider which images it's used for, and that this is highly dependent on context and not a decision that WordPress should be making bluntly for all sites like this. If even Google believed that lazy loading was preferable for all (or even most) contexts, they would have made it the default browser behaviour for loading images. So why should WordPress make this behaviour the default? I believe that at most WordPress should add a checkbox for enabling lazy loading to the image insert/edit modals.

Perhaps I've just had experience with poor implementations, but this just boils down to me really wanting to see more consideration of the user experience of lazy loading, as well as some articulation of what the performance benefits actually are if performance is the justification (and not bandwidth/memory usage).

Last edited 11 days ago by JakePT (previous) (diff)

#16 @mor10
11 days ago

@JakePT The main purpose of performance enhancements is exactly what you are asking for: Creating better user experiences for people on slow connections or with slower / older / less fancy devices. Reducing the weight of a page load, optimizing the delivery of assets, all these things benefit people on slow connections and/or devices more than it benefits people on broadband with top-of-the-line devices.

Lazy loading was created specifically to address page load for people on slower connections because of large page sizes mainly caused by images appearing "below the fold" and out of view. There's ample research to show people rarely scroll all the way down a page, and loading images out of view is a waste of valuable resources which incurs cost and slowness for the end-user, especially on slow connections. By lazy loading images and other heavy assets, the user is delivered a significantly improved user experience while at the same time saving both time and money.

The specific spec from Google, which is now implemented in Chrome, is to use the loading attribute to instruct the browser on how to lazy load images and iframes. By default, the browser treats all images and iframes as loading="auto" meaning the browser automatically lazy loads assets based on its own formula. Developers can set each asset to either loading="lazy" which "defers loading of the resource until it reaches a calculated distance from the viewport", or loading="eager" which "loads the resource immediately, regardless of where it's located on the page."

My proposal is to build this functionality into WP core to give developers and users direct access to this functionality:

  • Developers can tag individual images or image types with either lazy or eager to control how they are loaded. A good example would be featured images being set to eager because they typically appear at the top.
  • Users could be provided with a toggle to choose if they want individual images to load lazy or eager, but this is quite advanced and puts the onus on the end-user to know what lazy loading means and how it impacts performance.

Two crucial aspects of the proposal by Google are:

  1. Unlike current implementations of lazy loading which use JavaScript and an IntersectionObserver or similar to load images when they appear within the viewport, the new spec loads the image when it is within a "calculated distance threshold" (https://web.dev/native-lazy-loading#load-in-distance-threshold) meaning the image is likely to be loaded before it hits the viewport, thus reducing the risk of the scenario you are worried about with images not appearing when they are supposed to.
  2. The proposal is closely related to the intrinsicsize attribute proposal (https://github.com/WICG/intrinsicsize-attribute) which is slowly rolling out and will make it easier for browsers to clear space for yet-to-be-loaded images and other assets.

This ticket was mentioned in Slack in #core-js by pierlo. View the logs.


7 days ago

This ticket was mentioned in Slack in #core-js by pierlo. View the logs.


6 days ago

#19 follow-up: @pierlo
5 days ago

I've added lazy loading of images to theme screenshots, as @joyously suggested here.

I used the lazysizes library for implementing it, and added the native-loading plugin to automatically transform img.lazyload elements in browsers that support native lazy loading.

The media library page can also benefit from this.

#20 in reply to: ↑ 19 ; follow-up: @jonoaldersonwp
5 days ago

Replying to pierlo:

I've added lazy loading of images to theme screenshots, as @joyously suggested here.

I used the lazysizes library for implementing it, and added the native-loading plugin to automatically transform img.lazyload elements in browsers that support native lazy loading.

The media library page can also benefit from this.

Is there a reason for this approach, vs the native Chrome approach described above?

#21 @willstockstech
5 days ago

On the topic of lazysizes/intersection observer - this is something that was asked of Alexander (the creator of lazysizes), who recently wrote up a fairly insightful comment on the GitHub issue that was raised requesting it: https://github.com/aFarkas/lazysizes/issues/263#issuecomment-518340398

As was mentioned above, if lazysizes is being considered as a possible solution, there's a "native" plugin for lazysizes that caters for the browser native implementation of lazyloading: https://github.com/aFarkas/lazysizes/tree/gh-pages/plugins/native-loading

#22 in reply to: ↑ 20 @willstockstech
5 days ago

Replying to jonoaldersonwp:

Is there a reason for this approach, vs the native Chrome approach described above?

Native Chrome approach = Chrome only (https://caniuse.com/#search=loading - currently 0.15%)
lazysizes + native-loading plugin = multiple browsers (lazysizes for those that don't support the loading attribute, native for those that do)

#23 @jonoaldersonwp
5 days ago

Makes sense. However, that changes the spec and the requirements, and introduces a lot of complexity.

Moving to a data-src based approach has very different implications from an integration, standards, performance, accessibility and SEO perspective - even with the hybrid approach. It has many more moving parts than adding support for a new attribute. Handling this is not impossible, but it's much more nuanced.

I'd much rather we go launch with adding support for loading=lazy, and not change anything else; and that we 'pave the cowpath' by encouraging other browser vendors to catch up.

Last edited 5 days ago by jonoaldersonwp (previous) (diff)

#24 follow-ups: @mor10
4 days ago

Adding the new loading spec is non-destructive and adds no new loads on sites. Adding a JS library adds significant load and complexity. The current CanIUse stats for loading is based on a week-old release of Chrome. It will shoot up very fast. More importantly, WP adding loading will increase the percentage of sites using the spec exponentially, effectively carving a cowpath for the browsers to follow. This is very much a question of what role WP wants to play on the web: A leader in pushing the web forward, or a stalwart falling back on old tech when new solutions exist.

From my perspective there are two options: Either we implement the loading spec and move the web forward, or we do nothing. A JS solution runs counter to the idea of native lazy-loading and may end up delaying the implementation on the web significantly.

In my opinion.

#25 @jonoaldersonwp
4 days ago

What @mor10 said.

Last edited 4 days ago by jonoaldersonwp (previous) (diff)

#26 in reply to: ↑ 24 @marsjaninzmarsa
4 days ago

Replying to mor10:

Adding the new loading spec is non-destructive and adds no new loads on sites. Adding a JS library adds significant load and complexity. (…)

Totally agree. 😊

#27 @willstockstech
4 days ago

Sorry, I should've clarified!!! I don't think lazysizes should be included as part of core. There's too much configurability/complexity to it, I agree! My points were simply in relation to the other comments/questions around it :)

If core catered for lazysizes... it'd also have to cater for every configurable option/plugin - that would be quite a burden and is definitely still theme/plugin territory?

It would definitely be good to see the native lazyloading spec/attribute get a real push!!

#28 in reply to: ↑ 24 @pierlo
4 days ago

Replying to mor10:

Adding the new loading spec is non-destructive and adds no new loads on sites. Adding a JS library adds significant load and complexity. (...).

Agreed. So is it the consensus that the loading attribute should be added to 'pave the cowpath'?

#29 @drywallbmb
3 days ago

Love the idea. Let's make it happen!

@spacedmonkey
41 hours ago

#30 @spacedmonkey
41 hours ago

  • Keywords has-patch added; needs-patch removed

As a talking point, I have uploaded a first patch to this ticket. This uses a new feature flag. To enable lazy loading images, simply add theme support for lazy-loading-images. This is something I discussed with @joemcgill .

The function is designed to be easily to lazy loading to iframes later as well.

I don't personally believe we need a javascript shim here for backwards compability. We don't do this with srcsets correctly.

#31 @birgire
38 hours ago

Thanks for the patch @spacedmonkey

It seems that lazy loading is currently used in plugins in various ways.

I had a peek into the plugins directory:

https://wpdirectory.net/search/01DJFY0YHPHJ76JWB6EW5SXCNK

that contains image tags like:

<img data-src="...
<img data-lazy-src="...
<img data-lazyloaded="1"...
<img data-flickity-lazyload="...
<img data-gt-lazy-src="...
<img data-lazy="...

<img class="lazyload" ...
<img class="lazy lazy-hidden" data-lazy-type="iframe" data-lazy-src="...
<img class="supsystic-lazy" data-original="...
<img class="rocket-lazyload-rocket-logo" ...
<img class="lazy_load_image" ...
<img class="owl-lazy" data-src="...
<img class="ready-lazy" data-original="...
<img class="lazy" data-original="...
<img class="lazyOwl" ...
<img class="listing lazy" data-original="...
<img class="shopapp-lazy" data-original="...
<img class="responsively-lazy" ...
<img class="fullWidthfileImg lazy" data-original="...

<img data-no-lazy="1" src=
<img class="skip-lazy" src 
<img class="skip-lazy gmw-image"

Not all af these might be part of the content, but I wonder how mixing it with loading="lazy" will affect the already lazy loaded content images.

If they can't mix, then more aggressive checks come to mind to avoid it. Rather than checking only for the ' loading=' as in 44427.2.diff, one could avoid replacements on images that contain data attributes for original, src, lazy, lazy-src, source, ... etc. But I think that might still miss out lot of other cases. (filterable?)

Making the flag available to themes, rather than having it ON by default, sounds safer, at least to start with.

Last edited 38 hours ago by birgire (previous) (diff)

#32 @jonoaldersonwp
37 hours ago

They can mix fine. All of those approaches are just hooks for JS systems which watch for element visibility/similar. The native loading=lazy won't interfere with that (though it may be rendered redundant in some cases).

#33 @birgire
33 hours ago

@jonoaldersonwp thanks for clarifying that. I guess it would be like running both the native way and a JS fallback, yeah that was my worry. I will just try this out when I can :-)

I assume there might also be content cases like:

<img data-no-lazy="1" 
<img class="skip-lazy" 
...

i.e. if the user has explicitly set the content images to not lazy load.

I think those cases should be respected, if possible, so that they would not be lazy loaded regardless.

So in addition to defining a suitable flag to avoid this, the existing flags out there, should be respected, if possible.

#34 @jonoaldersonwp
32 hours ago

I've never seen an example like that in the wild. In every case (and library/approach, etc) I've ever seen, images are either 'vanilla', or have classes/attributes/properties to enable lazy loading. I've never seen an exclusion-based approach.

#35 @birgire
31 hours ago

JetPack supports exclusion of lazy loading images with the skip-lazy class or custom classes:

You can instruct Lazy Images to skip any image using the skip-lazy css class or any given class of your choice by using the jetpack_lazy_images_blacklisted_classes filter.

https://jetpack.com/support/lazy-images/

https://developer.jetpack.com/module/lazy-images/

Another example of exclusion with the data-no-lazy="1" attribute:

https://docs.wp-rocket.me/article/15-disabling-lazy-load-on-specific-images

Last edited 31 hours ago by birgire (previous) (diff)

#36 @jonoaldersonwp
21 hours ago

Gah. Trust those two to have frustrating anti-patterns.
Ok, yeah, we should sniff for those!

Note: See TracTickets for help on using tickets.