Make WordPress Core


Ignore:
Timestamp:
05/22/2023 07:11:36 PM (2 weeks ago)
Author:
flixos90
Message:

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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/media.php

    r55825 r55847  
    35603560     */
    35613561    public function test_wp_get_loading_attr_default( $context ) {
    3562         global $wp_query, $wp_the_query;
    3563 
    35643562        // Return 'lazy' by default.
    35653563        $this->assertSame( 'lazy', wp_get_loading_attr_default( 'test' ) );
     
    35693567        $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
    35703568
    3571         $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
     3569        $query = $this->get_new_wp_query_for_published_post();
    35723570        $this->reset_content_media_count();
    35733571        $this->reset_omit_loading_attr_filter();
     
    35803578
    35813579            // Set as main query.
    3582             $wp_the_query = $wp_query;
     3580            $this->set_main_query( $query );
    35833581
    35843582            // For contexts other than for the main content, still return 'lazy' even in the loop
     
    36143612     */
    36153613    public function test_wp_omit_loading_attr_threshold_filter() {
    3616         global $wp_query, $wp_the_query;
    3617 
    3618         $wp_query     = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
    3619         $wp_the_query = $wp_query;
     3614        $query = $this->get_new_wp_query_for_published_post();
     3615        $this->set_main_query( $query );
    36203616        $this->reset_content_media_count();
    36213617        $this->reset_omit_loading_attr_filter();
     
    36413637     */
    36423638    public function test_wp_filter_content_tags_with_wp_get_loading_attr_default() {
    3643         global $wp_query, $wp_the_query;
    3644 
    36453639        $img1         = get_image_tag( self::$large_id, '', '', '', 'large' );
    36463640        $iframe1      = '<iframe src="https://www.example.com" width="640" height="360"></iframe>';
     
    36603654        $content_expected   = wp_img_tag_add_decoding_attr( $content_expected, 'the_content' );
    36613655
    3662         $wp_query     = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
    3663         $wp_the_query = $wp_query;
     3656        $query = $this->get_new_wp_query_for_published_post();
     3657        $this->set_main_query( $query );
    36643658        $this->reset_content_media_count();
    36653659        $this->reset_omit_loading_attr_filter();
     
    36973691        $omit_threshold = wp_omit_loading_attr_threshold( true );
    36983692        $this->assertSame( 1, $omit_threshold );
     3693    }
     3694
     3695    /**
     3696     * Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header if not main query.
     3697     *
     3698     * @ticket 58211
     3699     *
     3700     * @covers ::wp_get_loading_attr_default
     3701     *
     3702     * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop
     3703     *
     3704     * @param string $context Context for the element for which the `loading` attribute value is requested.
     3705     */
     3706    public function test_wp_get_loading_attr_default_before_loop_if_not_main_query( $context ) {
     3707        global $wp_query;
     3708
     3709        $wp_query = $this->get_new_wp_query_for_published_post();
     3710        $this->reset_content_media_count();
     3711        $this->reset_omit_loading_attr_filter();
     3712
     3713        do_action( 'get_header' );
     3714
     3715        // Lazy if not main query.
     3716        $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
     3717    }
     3718
     3719    /**
     3720     * 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.
     3721     *
     3722     * @ticket 58211
     3723     *
     3724     * @covers ::wp_get_loading_attr_default
     3725     *
     3726     * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop
     3727     *
     3728     * @param string $context Context for the element for which the `loading` attribute value is requested.
     3729     */
     3730    public function test_wp_get_loading_attr_default_before_loop_in_main_query_but_header_not_called( $context ) {
     3731        global $wp_query;
     3732
     3733        $wp_query = $this->get_new_wp_query_for_published_post();
     3734        $this->set_main_query( $wp_query );
     3735        $this->reset_content_media_count();
     3736        $this->reset_omit_loading_attr_filter();
     3737
     3738        // Lazy if header not called.
     3739        $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
     3740    }
     3741
     3742    /**
     3743     * Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header for main query.
     3744     *
     3745     * @ticket 58211
     3746     *
     3747     * @covers ::wp_get_loading_attr_default
     3748     *
     3749     * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop
     3750     *
     3751     * @param string $context Context for the element for which the `loading` attribute value is requested.
     3752     */
     3753    public function test_wp_get_loading_attr_default_before_loop_if_main_query( $context ) {
     3754        global $wp_query;
     3755
     3756        $wp_query = $this->get_new_wp_query_for_published_post();
     3757        $this->set_main_query( $wp_query );
     3758        $this->reset_content_media_count();
     3759        $this->reset_omit_loading_attr_filter();
     3760
     3761        do_action( 'get_header' );
     3762        $this->assertFalse( wp_get_loading_attr_default( $context ) );
     3763    }
     3764
     3765    /**
     3766     * Tests that wp_get_loading_attr_default() returns the expected loading attribute value after get_header and after loop.
     3767     *
     3768     * @ticket 58211
     3769     *
     3770     * @covers ::wp_get_loading_attr_default
     3771     *
     3772     * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop
     3773     *
     3774     * @param string $context Context for the element for which the `loading` attribute value is requested.
     3775     */
     3776    public function test_wp_get_loading_attr_default_after_loop( $context ) {
     3777        global $wp_query;
     3778
     3779        $wp_query = $this->get_new_wp_query_for_published_post();
     3780        $this->set_main_query( $wp_query );
     3781        $this->reset_content_media_count();
     3782        $this->reset_omit_loading_attr_filter();
     3783
     3784        do_action( 'get_header' );
     3785
     3786        while ( have_posts() ) {
     3787            the_post();
     3788        }
     3789        $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
     3790    }
     3791
     3792    /**
     3793     * Tests that wp_get_loading_attr_default() returns the expected loading attribute if no loop.
     3794     *
     3795     * @ticket 58211
     3796     *
     3797     * @covers ::wp_get_loading_attr_default
     3798     *
     3799     * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop
     3800     *
     3801     * @param string $context Context for the element for which the `loading` attribute value is requested.
     3802     */
     3803    public function test_wp_get_loading_attr_default_no_loop( $context ) {
     3804        global $wp_query;
     3805
     3806        $wp_query = $this->get_new_wp_query_for_published_post();
     3807        $this->set_main_query( $wp_query );
     3808        $this->reset_content_media_count();
     3809        $this->reset_omit_loading_attr_filter();
     3810
     3811        // Ensure header and footer is called.
     3812        do_action( 'get_header' );
     3813        do_action( 'get_footer' );
     3814
     3815        // Load lazy if the there is no loop and footer was called.
     3816        $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
     3817    }
     3818
     3819    /**
     3820     * Data provider.
     3821     *
     3822     * @return array[]
     3823     */
     3824    public function data_wp_get_loading_attr_default_before_and_no_loop() {
     3825        return array(
     3826            array( 'wp_get_attachment_image' ),
     3827            array( 'the_post_thumbnail' ),
     3828        );
    36993829    }
    37003830
     
    41674297        );
    41684298    }
     4299
     4300    /**
     4301     * Returns a new WP_Query.
     4302     *
     4303     * @global WP_Query $wp_query WordPress Query object.
     4304     *
     4305     * @return WP_Query a new query.
     4306     */
     4307    public function get_new_wp_query_for_published_post() {
     4308        global $wp_query;
     4309
     4310        // New query to $wp_query. update global for the loop.
     4311        $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
     4312
     4313        return $wp_query;
     4314    }
     4315
     4316    /**
     4317     * Sets a query as main query.
     4318     *
     4319     * @global WP_Query $wp_the_query WordPress Query object.
     4320     *
     4321     * @param WP_Query $query query to be set as main query.
     4322     */
     4323    public function set_main_query( $query ) {
     4324        global $wp_the_query;
     4325        $wp_the_query = $query;
     4326    }
    41694327}
    41704328
Note: See TracChangeset for help on using the changeset viewer.