mirror of
git://develop.git.wordpress.org/
synced 2025-04-11 15:42:03 +02:00
Media: Conditionally skip lazy-loading on images before the loop to improve LCP performance.
When the logic to exclude images that likely appear above the fold from being lazy-loaded was introduced in WordPress 5.9, initially only images that appear within the main query loop were being considered. However, there is a good chance that images above the fold are rendered before the loop starts, for example in the header template part. It is particularly common for a theme to display the featured image for a single post in the header. Based on HTTP Archive data from February 2023, the majority of LCP images that are still being lazy-loaded on WordPress sites use the `wp-post-image` class, i.e. are featured images. This changeset enhances the logic in `wp_get_loading_attr_default()` to not lazy-load images that appear within or after the header template part and before the query loop, using a new `WP_Query::$before_loop` property. For block themes, this was for the most part already addressed in [55318], however this enhancement implements the solution in a more generally applicable way that brings the improvement to classic themes as well. Props thekt12, flixos90, spacedmonkey, costdev, zunaid321, mukesh27. Fixes #58211. See #53675, #56930. git-svn-id: https://develop.svn.wordpress.org/trunk@55847 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
parent
3c6184d81c
commit
71140f327f
@ -108,6 +108,14 @@ class WP_Query {
|
||||
*/
|
||||
public $current_post = -1;
|
||||
|
||||
/**
|
||||
* Whether the caller is before the loop.
|
||||
*
|
||||
* @since 6.3.0
|
||||
* @var bool
|
||||
*/
|
||||
public $before_loop = true;
|
||||
|
||||
/**
|
||||
* Whether the loop has started and the caller is in the loop.
|
||||
*
|
||||
@ -517,6 +525,7 @@ class WP_Query {
|
||||
$this->post_count = 0;
|
||||
$this->current_post = -1;
|
||||
$this->in_the_loop = false;
|
||||
$this->before_loop = true;
|
||||
unset( $this->request );
|
||||
unset( $this->post );
|
||||
unset( $this->comments );
|
||||
@ -3631,6 +3640,7 @@ class WP_Query {
|
||||
}
|
||||
|
||||
$this->in_the_loop = true;
|
||||
$this->before_loop = false;
|
||||
|
||||
if ( -1 == $this->current_post ) { // Loop has just started.
|
||||
/**
|
||||
@ -3671,6 +3681,8 @@ class WP_Query {
|
||||
// Do some cleaning up after the loop.
|
||||
$this->rewind_posts();
|
||||
} elseif ( 0 === $this->post_count ) {
|
||||
$this->before_loop = false;
|
||||
|
||||
/**
|
||||
* Fires if no results are found in a post query.
|
||||
*
|
||||
|
@ -5490,30 +5490,52 @@ function wp_get_webp_info( $filename ) {
|
||||
*
|
||||
* @since 5.9.0
|
||||
*
|
||||
* @global WP_Query $wp_query WordPress Query object.
|
||||
*
|
||||
* @param string $context Context for the element for which the `loading` attribute value is requested.
|
||||
* @return string|bool The default `loading` attribute value. Either 'lazy', 'eager', or a boolean `false`, to indicate
|
||||
* that the `loading` attribute should be skipped.
|
||||
*/
|
||||
function wp_get_loading_attr_default( $context ) {
|
||||
global $wp_query;
|
||||
|
||||
// Skip lazy-loading for the overall block template, as it is handled more granularly.
|
||||
if ( 'template' === $context ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not lazy-load images in the header block template part, as they are likely above the fold.
|
||||
// For classic themes, this is handled in the condition below using the 'get_header' action.
|
||||
$header_area = WP_TEMPLATE_PART_AREA_HEADER;
|
||||
if ( "template_part_{$header_area}" === $context ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Skip programmatically created images within post content as they need to be handled together with the other
|
||||
* images within the post content.
|
||||
* Without this clause, they would already be counted below which skews the number and can result in the first
|
||||
* post content image being lazy-loaded only because there are images elsewhere in the post content.
|
||||
*/
|
||||
if ( ( 'the_post_thumbnail' === $context || 'wp_get_attachment_image' === $context ) && doing_filter( 'the_content' ) ) {
|
||||
return false;
|
||||
// Special handling for programmatically created image tags.
|
||||
if ( ( 'the_post_thumbnail' === $context || 'wp_get_attachment_image' === $context ) ) {
|
||||
/*
|
||||
* Skip programmatically created images within post content as they need to be handled together with the other
|
||||
* images within the post content.
|
||||
* Without this clause, they would already be counted below which skews the number and can result in the first
|
||||
* post content image being lazy-loaded only because there are images elsewhere in the post content.
|
||||
*/
|
||||
if ( doing_filter( 'the_content' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Conditionally skip lazy-loading on images before the loop.
|
||||
if (
|
||||
// Only apply for main query but before the loop.
|
||||
$wp_query->before_loop && $wp_query->is_main_query()
|
||||
/*
|
||||
* Any image before the loop, but after the header has started should not be lazy-loaded,
|
||||
* except when the footer has already started which can happen when the current template
|
||||
* does not include any loop.
|
||||
*/
|
||||
&& did_action( 'get_header' ) && ! did_action( 'get_footer' )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -3559,8 +3559,6 @@ EOF;
|
||||
* @param string $context
|
||||
*/
|
||||
public 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' ) );
|
||||
@ -3568,7 +3566,7 @@ EOF;
|
||||
// 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'] ) ) );
|
||||
$query = $this->get_new_wp_query_for_published_post();
|
||||
$this->reset_content_media_count();
|
||||
$this->reset_omit_loading_attr_filter();
|
||||
|
||||
@ -3579,7 +3577,7 @@ EOF;
|
||||
$this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
|
||||
|
||||
// Set as main query.
|
||||
$wp_the_query = $wp_query;
|
||||
$this->set_main_query( $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.
|
||||
@ -3613,10 +3611,8 @@ EOF;
|
||||
* @ticket 53675
|
||||
*/
|
||||
public 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;
|
||||
$query = $this->get_new_wp_query_for_published_post();
|
||||
$this->set_main_query( $query );
|
||||
$this->reset_content_media_count();
|
||||
$this->reset_omit_loading_attr_filter();
|
||||
|
||||
@ -3640,8 +3636,6 @@ EOF;
|
||||
* @ticket 53675
|
||||
*/
|
||||
public 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' );
|
||||
@ -3659,8 +3653,8 @@ EOF;
|
||||
$content_expected = $img1 . $iframe1 . $lazy_img2 . $lazy_img3 . $lazy_iframe2;
|
||||
$content_expected = wp_img_tag_add_decoding_attr( $content_expected, 'the_content' );
|
||||
|
||||
$wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
|
||||
$wp_the_query = $wp_query;
|
||||
$query = $this->get_new_wp_query_for_published_post();
|
||||
$this->set_main_query( $query );
|
||||
$this->reset_content_media_count();
|
||||
$this->reset_omit_loading_attr_filter();
|
||||
|
||||
@ -3698,6 +3692,142 @@ EOF;
|
||||
$this->assertSame( 1, $omit_threshold );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header if not main query.
|
||||
*
|
||||
* @ticket 58211
|
||||
*
|
||||
* @covers ::wp_get_loading_attr_default
|
||||
*
|
||||
* @dataProvider data_wp_get_loading_attr_default_before_and_no_loop
|
||||
*
|
||||
* @param string $context Context for the element for which the `loading` attribute value is requested.
|
||||
*/
|
||||
public function test_wp_get_loading_attr_default_before_loop_if_not_main_query( $context ) {
|
||||
global $wp_query;
|
||||
|
||||
$wp_query = $this->get_new_wp_query_for_published_post();
|
||||
$this->reset_content_media_count();
|
||||
$this->reset_omit_loading_attr_filter();
|
||||
|
||||
do_action( 'get_header' );
|
||||
|
||||
// Lazy if not main query.
|
||||
$this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header in main query but header was not called.
|
||||
*
|
||||
* @ticket 58211
|
||||
*
|
||||
* @covers ::wp_get_loading_attr_default
|
||||
*
|
||||
* @dataProvider data_wp_get_loading_attr_default_before_and_no_loop
|
||||
*
|
||||
* @param string $context Context for the element for which the `loading` attribute value is requested.
|
||||
*/
|
||||
public function test_wp_get_loading_attr_default_before_loop_in_main_query_but_header_not_called( $context ) {
|
||||
global $wp_query;
|
||||
|
||||
$wp_query = $this->get_new_wp_query_for_published_post();
|
||||
$this->set_main_query( $wp_query );
|
||||
$this->reset_content_media_count();
|
||||
$this->reset_omit_loading_attr_filter();
|
||||
|
||||
// Lazy if header not called.
|
||||
$this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header for main query.
|
||||
*
|
||||
* @ticket 58211
|
||||
*
|
||||
* @covers ::wp_get_loading_attr_default
|
||||
*
|
||||
* @dataProvider data_wp_get_loading_attr_default_before_and_no_loop
|
||||
*
|
||||
* @param string $context Context for the element for which the `loading` attribute value is requested.
|
||||
*/
|
||||
public function test_wp_get_loading_attr_default_before_loop_if_main_query( $context ) {
|
||||
global $wp_query;
|
||||
|
||||
$wp_query = $this->get_new_wp_query_for_published_post();
|
||||
$this->set_main_query( $wp_query );
|
||||
$this->reset_content_media_count();
|
||||
$this->reset_omit_loading_attr_filter();
|
||||
|
||||
do_action( 'get_header' );
|
||||
$this->assertFalse( wp_get_loading_attr_default( $context ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that wp_get_loading_attr_default() returns the expected loading attribute value after get_header and after loop.
|
||||
*
|
||||
* @ticket 58211
|
||||
*
|
||||
* @covers ::wp_get_loading_attr_default
|
||||
*
|
||||
* @dataProvider data_wp_get_loading_attr_default_before_and_no_loop
|
||||
*
|
||||
* @param string $context Context for the element for which the `loading` attribute value is requested.
|
||||
*/
|
||||
public function test_wp_get_loading_attr_default_after_loop( $context ) {
|
||||
global $wp_query;
|
||||
|
||||
$wp_query = $this->get_new_wp_query_for_published_post();
|
||||
$this->set_main_query( $wp_query );
|
||||
$this->reset_content_media_count();
|
||||
$this->reset_omit_loading_attr_filter();
|
||||
|
||||
do_action( 'get_header' );
|
||||
|
||||
while ( have_posts() ) {
|
||||
the_post();
|
||||
}
|
||||
$this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that wp_get_loading_attr_default() returns the expected loading attribute if no loop.
|
||||
*
|
||||
* @ticket 58211
|
||||
*
|
||||
* @covers ::wp_get_loading_attr_default
|
||||
*
|
||||
* @dataProvider data_wp_get_loading_attr_default_before_and_no_loop
|
||||
*
|
||||
* @param string $context Context for the element for which the `loading` attribute value is requested.
|
||||
*/
|
||||
public function test_wp_get_loading_attr_default_no_loop( $context ) {
|
||||
global $wp_query;
|
||||
|
||||
$wp_query = $this->get_new_wp_query_for_published_post();
|
||||
$this->set_main_query( $wp_query );
|
||||
$this->reset_content_media_count();
|
||||
$this->reset_omit_loading_attr_filter();
|
||||
|
||||
// Ensure header and footer is called.
|
||||
do_action( 'get_header' );
|
||||
do_action( 'get_footer' );
|
||||
|
||||
// Load lazy if the there is no loop and footer was called.
|
||||
$this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function data_wp_get_loading_attr_default_before_and_no_loop() {
|
||||
return array(
|
||||
array( 'wp_get_attachment_image' ),
|
||||
array( 'the_post_thumbnail' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that wp_filter_content_tags() does not add loading="lazy" to the first
|
||||
* image in the loop when using a block theme.
|
||||
@ -4166,6 +4296,34 @@ EOF;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new WP_Query.
|
||||
*
|
||||
* @global WP_Query $wp_query WordPress Query object.
|
||||
*
|
||||
* @return WP_Query a new query.
|
||||
*/
|
||||
public function get_new_wp_query_for_published_post() {
|
||||
global $wp_query;
|
||||
|
||||
// New query to $wp_query. update global for the loop.
|
||||
$wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
|
||||
|
||||
return $wp_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a query as main query.
|
||||
*
|
||||
* @global WP_Query $wp_the_query WordPress Query object.
|
||||
*
|
||||
* @param WP_Query $query query to be set as main query.
|
||||
*/
|
||||
public function set_main_query( $query ) {
|
||||
global $wp_the_query;
|
||||
$wp_the_query = $query;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -897,4 +897,71 @@ class Tests_Query extends WP_UnitTestCase {
|
||||
$this->assertFalse( $q->is_tax() );
|
||||
$this->assertFalse( $q->is_tag( 'non-existent-tag' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if $before_loop is true before loop.
|
||||
*
|
||||
* @ticket 58211
|
||||
*/
|
||||
public function test_before_loop_value_set_true_before_the_loop() {
|
||||
// Get a new query with 3 posts.
|
||||
$query = $this->get_new_wp_query_with_posts( 3 );
|
||||
|
||||
$this->assertTrue( $query->before_loop );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test $before_loop value is set to false when the loop starts.
|
||||
*
|
||||
* @ticket 58211
|
||||
*
|
||||
* @covers WP_Query::the_post
|
||||
*/
|
||||
public function test_before_loop_value_set_to_false_in_loop_with_post() {
|
||||
// Get a new query with 2 posts.
|
||||
$query = $this->get_new_wp_query_with_posts( 2 );
|
||||
|
||||
while ( $query->have_posts() ) {
|
||||
// $before_loop should be set false as soon as the_post is called for the first time.
|
||||
$query->the_post();
|
||||
|
||||
$this->assertFalse( $query->before_loop );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test $before_loop value is set to false when there is no post in the loop.
|
||||
*
|
||||
* @ticket 58211
|
||||
*
|
||||
* @covers WP_Query::have_posts
|
||||
*/
|
||||
public function test_before_loop_set_false_after_loop_with_no_post() {
|
||||
// New query without any posts in the result.
|
||||
$query = new WP_Query(
|
||||
array(
|
||||
'category_name' => 'non-existent-category',
|
||||
)
|
||||
);
|
||||
|
||||
// There will not be any posts, so the loop will never actually enter.
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
}
|
||||
|
||||
// Still, this should be false as there are no results and entering the loop was attempted.
|
||||
$this->assertFalse( $query->before_loop );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new query with a given number of posts.
|
||||
*
|
||||
* @param int $no_of_posts Number of posts to be added in the query.
|
||||
*/
|
||||
public function get_new_wp_query_with_posts( $no_of_posts ) {
|
||||
$post_ids = self::factory()->post->create_many( $no_of_posts );
|
||||
$query = new WP_Query( array( 'post__in' => $post_ids ) );
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user