Media: Refine the heuristics to exclude certain images and iframes from being lazy-loaded to improve performance.

This changeset implements the refined lazy-loading behavior outlined in https://make.wordpress.org/core/2021/07/15/refining-wordpress-cores-lazy-loading-implementation/ in order to improve the Largest Contentful Paint metric, which can see a regression from images or iframes above the fold being lazy-loaded. Adjusting this so far has been possible for developers via filters and still is, however this enhancement brings a more accurate behavior out of the box for the majority of themes.

Specifically, this changeset skips the very first "content image or iframe" on the page from being lazy-loaded. "Content image or iframe" denotes any image or iframe that is found within content of any post in the current main query loop as well as any featured image of such a post. This applies both to "singular" as well as "archive" content: On a "singular" page the first image/iframe of the post is not lazy-loaded, while on an "archive" page the first image/iframe of the _first_ post in the query is not lazy-loaded.

This approach refines the lazy-loading behavior correctly for the majority of themes, which use a single-column layout for post content. For themes with multi-column layouts, a new `wp_omit_loading_attr_threshold` filter can be used to change how many of the first images/iframes are being skipped from lazy-loaded (default is `1`). For example, a theme using a three-column grid of latest posts for archives could use the filter to override the threshold to `3` on archive pages, so that the first three content images/iframes would not be lazy-loaded.

Props adamsilverstein, azaozz, flixos90, hellofromtonya, jonoaldersonwp, mte90, rviscomi, tweetythierry, westonruter.
Fixes #53675. See #50425.


git-svn-id: https://develop.svn.wordpress.org/trunk@52065 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Felix Arntz
2021-11-09 00:34:17 +00:00
parent fab9d0685e
commit 8649d6d4ff
4 changed files with 329 additions and 34 deletions

View File

@@ -3024,6 +3024,7 @@ EOF;
/**
* @ticket 50425
* @ticket 53463
* @ticket 53675
* @dataProvider data_wp_lazy_loading_enabled_context_defaults
*
* @param string $context Function context.
@@ -3046,6 +3047,7 @@ EOF;
'widget_block_content => true' => array( 'widget_block_content', true ),
'get_avatar => true' => array( 'get_avatar', true ),
'arbitrary context => true' => array( 'something_completely_arbitrary', true ),
'the_post_thumbnail => true' => array( 'the_post_thumbnail', true ),
);
}
@@ -3186,6 +3188,178 @@ EOF;
array( 'trash-attachment', '/?attachment_id=%ID%', false ),
);
}
/**
* @ticket 53675
* @dataProvider data_wp_get_loading_attr_default
*
* @param string $context
*/
function test_wp_get_loading_attr_default( $context ) {
global $wp_query, $wp_the_query;
// Return 'lazy' by default.
$this->assertSame( 'lazy', wp_get_loading_attr_default( 'test' ) );
$this->assertSame( 'lazy', wp_get_loading_attr_default( 'wp_get_attachment_image' ) );
// Return 'lazy' if not in the loop or the main query.
$this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
$wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
$this->reset_content_media_count();
$this->reset_omit_loading_attr_filter();
while ( have_posts() ) {
the_post();
// Return 'lazy' if in the loop but not in the main query.
$this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
// Set as main query.
$wp_the_query = $wp_query;
// For contexts other than for the main content, still return 'lazy' even in the loop
// and in the main query, and do not increase the content media count.
$this->assertSame( 'lazy', wp_get_loading_attr_default( 'wp_get_attachment_image' ) );
// Return `false` if in the loop and in the main query and it is the first element.
$this->assertFalse( wp_get_loading_attr_default( $context ) );
// Return 'lazy' if in the loop and in the main query for any subsequent elements.
$this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
// Yes, for all subsequent elements.
$this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
}
}
function data_wp_get_loading_attr_default() {
return array(
array( 'the_content' ),
array( 'the_post_thumbnail' ),
);
}
/**
* @ticket 53675
*/
function test_wp_omit_loading_attr_threshold_filter() {
global $wp_query, $wp_the_query;
$wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
$wp_the_query = $wp_query;
$this->reset_content_media_count();
$this->reset_omit_loading_attr_filter();
// Use the filter to alter the threshold for not lazy-loading to the first three elements.
add_filter(
'wp_omit_loading_attr_threshold',
function() {
return 3;
}
);
while ( have_posts() ) {
the_post();
// Due to the filter, now the first three elements should not be lazy-loaded, i.e. return `false`.
for ( $i = 0; $i < 3; $i++ ) {
$this->assertFalse( wp_get_loading_attr_default( 'the_content' ) );
}
// For following elements, lazy-load them again.
$this->assertSame( 'lazy', wp_get_loading_attr_default( 'the_content' ) );
}
}
/**
* @ticket 53675
*/
function test_wp_filter_content_tags_with_wp_get_loading_attr_default() {
global $wp_query, $wp_the_query;
$img1 = get_image_tag( self::$large_id, '', '', '', 'large' );
$iframe1 = '<iframe src="https://www.example.com" width="640" height="360"></iframe>';
$img2 = get_image_tag( self::$large_id, '', '', '', 'medium' );
$img3 = get_image_tag( self::$large_id, '', '', '', 'thumbnail' );
$iframe2 = '<iframe src="https://wordpress.org" width="640" height="360"></iframe>';
$lazy_img2 = wp_img_tag_add_loading_attr( $img2, 'the_content' );
$lazy_img3 = wp_img_tag_add_loading_attr( $img3, 'the_content' );
$lazy_iframe2 = wp_iframe_tag_add_loading_attr( $iframe2, 'the_content' );
// Use a threshold of 2.
add_filter(
'wp_omit_loading_attr_threshold',
function() {
return 2;
}
);
// Following the threshold of 2, the first two content media elements should not be lazy-loaded.
$content_unfiltered = $img1 . $iframe1 . $img2 . $img3 . $iframe2;
$content_expected = $img1 . $iframe1 . $lazy_img2 . $lazy_img3 . $lazy_iframe2;
$wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
$wp_the_query = $wp_query;
$this->reset_content_media_count();
$this->reset_omit_loading_attr_filter();
while ( have_posts() ) {
the_post();
add_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
$content_filtered = wp_filter_content_tags( $content_unfiltered, 'the_content' );
remove_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
}
// After filtering, the first image should not be lazy-loaded while the other ones should be.
$this->assertSame( $content_expected, $content_filtered );
}
/**
* @ticket 53675
*/
public function test_wp_omit_loading_attr_threshold() {
$this->reset_omit_loading_attr_filter();
// Apply filter, ensure default value of 1.
$omit_threshold = wp_omit_loading_attr_threshold();
$this->assertSame( 1, $omit_threshold );
// Add a filter that changes the value to 3. However, the filter is not applied a subsequent time in a single
// page load by default, so the value is still 1.
add_filter(
'wp_omit_loading_attr_threshold',
function() {
return 3;
}
);
$omit_threshold = wp_omit_loading_attr_threshold();
$this->assertSame( 1, $omit_threshold );
// Only by enforcing a fresh check, the filter gets re-applied.
$omit_threshold = wp_omit_loading_attr_threshold( true );
$this->assertSame( 3, $omit_threshold );
}
private function reset_content_media_count() {
// Get current value without increasing.
$content_media_count = wp_increase_content_media_count( 0 );
// Decrease it by its current value to "reset" it back to 0.
wp_increase_content_media_count( - $content_media_count );
}
private function reset_omit_loading_attr_filter() {
// Add filter to "reset" omit threshold back to null (unset).
add_filter( 'wp_omit_loading_attr_threshold', '__return_null', 100 );
// Force filter application to re-run.
wp_omit_loading_attr_threshold( true );
// Clean up the above filter.
remove_filter( 'wp_omit_loading_attr_threshold', '__return_null', 100 );
}
}
/**