ntext ) { return $loading_attrs; } // For now this function only supports images and iframes. if ( 'img' !== $tag_name && 'iframe' !== $tag_name ) { return $loading_attrs; } // For any resources, width and height must be provided, to avoid layout shifts. if ( ! isset( $attr['width'], $attr['height'] ) ) { return $loading_attrs; } if ( isset( $attr['loading'] ) ) { /* * While any `loading` value could be set in `$loading_attrs`, for * consistency we only do it for `loading="lazy"` since that is the * only possible value that WordPress core would apply on its own. */ if ( 'lazy' === $attr['loading'] ) { $loading_attrs['loading'] = 'lazy'; if ( isset( $attr['fetchpriority'] ) && 'high' === $attr['fetchpriority'] ) { _doing_it_wrong( __FUNCTION__, __( 'An image should not be lazy-loaded and marked as high priority at the same time.' ), '6.3.0' ); } } return $postprocess( $loading_attrs, true ); } // An image with `fetchpriority="high"` cannot be assigned `loading="lazy"` at the same time. if ( isset( $attr['fetchpriority'] ) && 'high' === $attr['fetchpriority'] ) { return $postprocess( $loading_attrs, true ); } /* * 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 ) { // Increase media count if there are images in header above a certian minimum size threshold. $maybe_increase_content_media_count(); return $postprocess( $loading_attrs, true ); } // The custom header image is always expected to be in the header. if ( 'get_header_image_tag' === $context ) { // Increase media count if there are images in header above a certian minimum size threshold. $maybe_increase_content_media_count(); return $postprocess( $loading_attrs, true ); } // Special handling for programmatically created image tags. if ( 'the_post_thumbnail' === $context || 'wp_get_attachment_image' === $context || 'widget_media_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 considered below which skews the image count and can result in * the first post content image being lazy-loaded or an image further down the page being marked as a high * priority. */ if ( doing_filter( 'the_content' ) ) { return $loading_attrs; } // 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' ) ) { // Increase media count if there are images in header above a certian minimum size threshold. $maybe_increase_content_media_count(); return $postprocess( $loading_attrs, true ); } } /* * The first elements in 'the_content' or 'the_post_thumbnail' should not be lazy-loaded, * as they are likely above the fold. Shortcodes are processed after content images, so if * thresholds haven't already been met, apply the same logic to those as well. */ if ( 'the_content' === $context || 'the_post_thumbnail' === $context || 'do_shortcode' === $context ) { // Only elements within the main query loop have special handling. if ( is_admin() || ! in_the_loop() || ! is_main_query() ) { $loading_attrs['loading'] = 'lazy'; return $postprocess( $loading_attrs, false ); } // Increase the counter since this is a main query content element. $content_media_count = wp_increase_content_media_count(); // If the count so far is below the threshold, `loading` attribute is omitted. if ( $content_media_count <= wp_omit_loading_attr_threshold() ) { // The first largest image will still get `fetchpriority='high'`. return $postprocess( $loading_attrs, true ); } } // Lazy-load by default for any unknown context. $loading_attrs['loading'] = 'lazy'; return $postprocess( $loading_attrs, false ); } /** * Gets the threshold for how many of the first content media elements to not lazy-load. * * This function runs the {@see 'wp_omit_loading_attr_threshold'} filter, which uses a default threshold value of 3. * The filter is only run once per page load, unless the `$force` parameter is used. * * @since 5.9.0 * * @param bool $force Optional. If set to true, the filter will be (re-)applied even if it already has been before. * Default false. * @return int The number of content media elements to not lazy-load. */ function wp_omit_loading_attr_threshold( $force = false ) { static $omit_threshold; // This function may be called multiple times. Run the filter only once per page load. if ( ! isset( $omit_threshold ) || $force ) { /** * Filters the threshold for how many of the first content media elements to not lazy-load. * * For these first content media elements, the `loading` attribute will be omitted. By default, this is the case * for only the very first content media element. * * @since 5.9.0 * @since 6.3.0 The default threshold was changed from 1 to 3. * * @param int $omit_threshold The number of media elements where the `loading` attribute will not be added. Default 3. */ $omit_threshold = apply_filters( 'wp_omit_loading_attr_threshold', 3 ); } return $omit_threshold; } /** * Increases an internal content media count variable. * * @since 5.9.0 * @access private * * @param int $amount Optional. Amount to increase by. Default 1. * @return int The latest content media count, after the increase. */ function wp_increase_content_media_count( $amount = 1 ) { static $content_media_count = 0; $content_media_count += $amount; return $content_media_count; } /** * Determines whether to add `fetchpriority='high'` to loading attributes. * * @since 6.3.0 * @access private * * @param array $loading_attrs Array of the loading optimization attributes for the element. * @param string $tag_name The tag name. * @param array $attr Array of the attributes for the element. * @return array Updated loading optimization attributes for the element. */ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr ) { // For now, adding `fetchpriority="high"` is only supported for images. if ( 'img' !== $tag_name ) { return $loading_attrs; } if ( isset( $attr['fetchpriority'] ) ) { /* * While any `fetchpriority` value could be set in `$loading_attrs`, * for consistency we only do it for `fetchpriority="high"` since that * is the only possible value that WordPress core would apply on its * own. */ if ( 'high' === $attr['fetchpriority'] ) { $loading_attrs['fetchpriority'] = 'high'; wp_high_priority_element_flag( false ); } return $loading_attrs; } // Lazy-loading and `fetchpriority="high"` are mutually exclusive. if ( isset( $loading_attrs['loading'] ) && 'lazy' === $loading_attrs['loading'] ) { return $loading_attrs; } if ( ! wp_high_priority_element_flag() ) { return $loading_attrs; } /** * Filters the minimum square-pixels threshold for an image to be eligible as the high-priority image. * * @since 6.3.0 * * @param int $threshold Minimum square-pixels threshold. Default 50000. */ $wp_min_priority_img_pixels = apply_filters( 'wp_min_priority_img_pixels', 50000 ); if ( $wp_min_priority_img_pixels <= $attr['width'] * $attr['height'] ) { $loading_attrs['fetchpriority'] = 'high'; wp_high_priority_element_flag( false ); } return $loading_attrs; } /** * Accesses a flag that indicates if an element is a possible candidate for `fetchpriority='high'`. * * @since 6.3.0 * @access private * * @param bool $value Optional. Used to change the static variable. Default null. * @return bool Returns true if high-priority element was marked already, otherwise false. */ function wp_high_priority_element_flag( $value = null ) { static $high_priority_element = true; if ( is_bool( $value ) ) { $high_priority_element = $value; } return $high_priority_element; }