Image loading optimization enhancements in 6.4

WordPress 6.4 comes with several enhancements to the wp_get_loading_optimization_attributes() function which was introduced in 6.3 as a central place to manage loading optimization attributes, specifically for images and iframes.

Quick recap: Loading optimization attributes are those such as loading="lazy" or fetchpriority="high", which can be added to certain HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. tags to optimize loading performance in the browser.

Simplified complex logic

The logic of the wp_get_loading_optimization_attributes() is generally quite complex, as it caters for several factors that influence when to apply certain loading optimization attributes. When the function was originally introduced, it was particularly hard to follow, since it included lots of early returns as well as closures. This was mostly due to maintaining its original code paths that came from the deprecated wp_get_loading_attr_default() function as much as possible to avoid breakage.

While the function logic is still complex in WordPress 6.4, it has been notably simplified, taking a more sequential and thus easier to follow approach. This facilitated the implementation of further enhancements such as those outlined below.

Please refer to TracTrac An open source project by Edgewall Software that serves as a bug tracker and project management tool for WordPress. ticketticket Created for both bug reports and feature development on the bug tracker. #58891 for additional details on these changes.

Managing the decoding="async" attribute

The decoding="async" attribute has been present on images by default since WordPress 6.1 (see Trac ticket #53232). As of WordPress 6.4, the logic for applying the attribute has been consolidated in the wp_get_loading_optimization_attributes() function, as the attribute is a perfect fit for that function.

Deprecated function

As part of that change, the wp_img_tag_add_decoding_attr() function has been deprecated, as its logic is now incorporated into wp_img_tag_add_loading_optimization_attrs(). Unless you are using the now deprecated function in your code, no changes should be needed after this update in regards to the decoding="async" attribute. The change merely enables control over the attribute in a more consistent manner (also see the new filters introduced below).

If you are using the deprecated function and want to use a fully backward compatible replacement, you can implement a custom wrapper function such as the following:

function myplugin_img_tag_add_decoding_attr( $image, $context ) {
	global $wp_version;

	// For WP >= 6.4.
	if ( version_compare( $wp_version, '6.4', '>=' ) ) {
		$image = wp_img_tag_add_loading_optimization_attrs( $image, $context );

		// Strip potential attributes added other than `decoding="async"`.
		return str_replace(
			array(
				' loading="lazy"',
				' fetchpriority="high"',
			),
			'',
			$image
		);
	}
 
    // For WP < 6.4.
    return wp_img_tag_add_decoding_attr( $image, $context );
}

Please refer to Trac ticket #58892 for additional details on these changes.

New filters to control loading optimization attributes

With WordPress 6.4, two filters have been added to wp_get_loading_optimization_attributes() which allow modifying or completely overriding the logic used to apply the loading optimization attributes:

  • The wp_get_loading_optimization_attributes filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. can be used to modify the results from the WordPress coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. logic.
  • The pre_wp_get_loading_optimization_attributes filter can be used to use entirely custom logic and effectively short-circuit the core function by returning a value other than false.

Below are a few examples on how these two filters could be used.

Filter usage examples

You could use the wp_get_loading_optimization_attributes filter to ensure a specific image within post content receives the fetchpriority="high" attribute while other ones do not:

function set_fetchpriority_high_on_specific_image( $loading_attrs, $tag_name, $attr, $context ) {
	if ( 'img' === $tag_name ) {
		if (
			'the_content' === $context &&
			isset( $attr['src'] ) &&
			$attr['src'] === 'https://example.org/a-specific-image.jpg'
		) {
			$loading_attrs['fetchpriority'] = 'high';
		} else {
			unset( $loading_attrs['fetchpriority'] );
		}
	}
	return $loading_attrs;
}
add_filter(
	'wp_get_loading_optimization_attributes',
	'set_fetchpriority_high_on_specific_image',
	10,
	4
);

Alternatively, you could use the wp_get_loading_optimization_attributes filter to disable adding the fetchpriority="high" attribute entirely:

function disable_fetchpriority_high( $loading_attrs ) {
	unset( $loading_attrs['fetchpriority'] );
	return $loading_attrs;
}
add_filter(
	'wp_get_loading_optimization_attributes',
	'disable_fetchpriority_high'
);

You could implement entirely custom logic in a pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party to detect which images appear in the viewport using client-side JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. logic or an external service and replace the function’s default logic with that:

function override_loading_optimization_attributes( $override, $tag_name, $attr, $context ) {
	// Bail if another filter callback already overrode this.
	if ( false !== $override ) {
		return $override;
	}

	// Use custom logic to determine whether image is LCP and whether it appears above the fold.
	if ( 'img' === $tag_name ) {
		$is_lcp      = custom_function_to_detect_whether_image_is_lcp_element( $attr );
		$in_viewport = custom_function_to_detect_whether_image_is_above_the_fold( $attr );

		/*
		 * Always add `decoding="async"`.
		 * Add `fetchpriority="high"` only to the LCP image.
		 * Add `loading="lazy"` to any image below the fold / outside the viewport.
		 */
		$loading_attrs = array( 'decoding' => 'async' );
		if ( $is_lcp ) {
			$loading_attrs['fetchpriority'] = 'high';
		} elseif ( ! $in_viewport ) {
			$loading_attrs['loading'] = 'lazy';
		}
		return $loading_attrs;
	}

	return $override;
}
add_filter(
	'pre_wp_get_loading_optimization_attributes',
	'override_loading_optimization_attributes',
	10,
	4
);

Please refer to Trac ticket #58893 for additional details on these changes.

Support for custom context values

As explained in the 6.3 dev note for the wp_get_loading_optimization_attributes() function, initially it only supported specific context values used by WordPress core. This made it confusing to write custom code making use of that function as you would have been forced to use a context string used elsewhere in core in order to get the performance benefits.

This limitation has been addressed in WordPress 6.4: The function now supports arbitrary contexts, and for the most part does not apply context-specific optimizations. There are two exceptions to this which are the “template_part_header” and “get_header_image_tag” contexts. Images within these contexts will always be interpreted to be in the headerHeader The header of your site is typically the first thing people will experience. The masthead or header art located across the top of your page is part of the look and feel of your website. It can influence a visitor’s opinion about your content and you/ your organization’s brand. It may also look different on different screen sizes.. The list of these two context strings is filterable with a new wp_loading_optimization_force_header_contexts filter, which allows images with other contexts to be always assumed to appear above the fold.

Context filter usage example

Below is an example: Assume that your plugin has a particular header image rendering function where it is safe to assume any image rendered with that function appears above the fold. You can rely on an arbitrary context specific to your function and then force WordPress core to consider images with that context to appear above the fold.

function force_myplugin_special_header_context_above_the_fold( $header_contexts ) {
	$header_contexts['myplugin_special_header'] = true;
	return $header_contexts;
}
add_filter(
	'wp_loading_optimization_force_header_contexts',
	'force_myplugin_special_header_context_above_the_fold'
);

Your image rendering function then needs to call wp_get_loading_optimization_attributes( 'img', $attr, 'myplugin_special_header' ) for the above filter to take effect.

Please refer to Trac ticket #58894 for additional details on these changes.

Hook priority change for wp_filter_content_tags()

The wp_filter_content_tags() was originally introduced in WordPress 5.5 as the foundation for lazy-loading as well as other optimizations for certain tags in a content blob. More recently a problem was identified that the function is called earlier than do_shortcode(), which means that any images added by shortcodes will not be able to make use of the performance benefits from wp_filter_content_tags().

To address that limitation, the hook priority with which wp_filter_content_tags() is hooked into the various filters (“the_content”, “the_excerpt”, “widget_text_content”, and “widget_block_content”) has been changed from the default 10 to 12. For context, the do_shortcode() function has hook priority 11.

While this is technically a breaking change, careful consideration and research across the WordPress plugin directory went into this decision. No plugins in the directory are affected by this change, given that there is only limited direct usage of the wp_filter_content_tags() function, and such usage has not been specific to the core filters. Still, in case you use the wp_filter_content_tags() function directly in your code, you may want to double check that this hook priority change does not result in a problem.

If your plugin processes custom content with the wp_filter_content_tags() function, it is encouraged to call that function after parsing other content such as blocks and shortcodes. The core change will not cause problems if that is currently not the case in your code, however it is recommended to have your custom logic follow a similar order.

Please refer to Trac ticket #58853 for additional details on these changes.

Props to @westonruter and @webcommsat for review and proofreading.

#6-4, #dev-notes, #dev-notes-6-4, #performance