diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
index 25d98ede38..4933e3f88f 100644
--- a/src/wp-includes/default-filters.php
+++ b/src/wp-includes/default-filters.php
@@ -324,6 +324,7 @@ add_filter( 'rest_authentication_errors', 'rest_cookie_check_errors', 100 );
add_action( 'wp_head', '_wp_render_title_tag', 1 );
add_action( 'wp_head', 'wp_enqueue_scripts', 1 );
add_action( 'wp_head', 'wp_resource_hints', 2 );
+add_action( 'wp_head', 'wp_preload_resources', 1 );
add_action( 'wp_head', 'feed_links', 2 );
add_action( 'wp_head', 'feed_links_extra', 3 );
add_action( 'wp_head', 'rsd_link' );
diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php
index c91cd48efa..0a12a590cf 100644
--- a/src/wp-includes/general-template.php
+++ b/src/wp-includes/general-template.php
@@ -3423,6 +3423,120 @@ function wp_resource_hints() {
}
}
+/**
+ * Prints resource preloads directives to browsers.
+ *
+ * Gives directive to browsers to preload specific resources that website will
+ * need very soon, this ensures that they are available earlier and are less
+ * likely to block the page's render. Preload directives should not be used for
+ * non-render-blocking elements, as then they would compete with the
+ * render-blocking ones, slowing down the render.
+ *
+ * These performance improving indicators work by using ``.
+ *
+ * @link https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload
+ * @link https://web.dev/preload-responsive-images/
+ *
+ * @since 6.1.0
+ */
+function wp_preload_resources() {
+ /**
+ * Filters domains and URLs for resource preloads.
+ *
+ * @since 6.1.0
+ *
+ * @param array $preload_resources {
+ * Array of resources and their attributes, or URLs to print for resource preloads.
+ *
+ * @type array ...$0 {
+ * Array of resource attributes.
+ *
+ * @type string $href URL to include in resource preloads. Required.
+ * @type string $as How the browser should treat the resource
+ * (`script`, `style`, `image`, `document`, etc).
+ * @type string $crossorigin Indicates the CORS policy of the specified resource.
+ * @type string $type Type of the resource (`text/html`, `text/css`, etc).
+ * @type string $media Accepts media types or media queries. Allows responsive preloading.
+ * @type string $imagesizes Responsive source size to the source Set.
+ * @type string $imagesrcset Responsive image sources to the source set.
+ * }
+ * }
+ */
+ $preload_resources = apply_filters( 'wp_preload_resources', array() );
+
+ if ( ! is_array( $preload_resources ) ) {
+ return;
+ }
+
+ $unique_resources = array();
+
+ // Parse the complete resource list and extract unique resources.
+ foreach ( $preload_resources as $resource ) {
+ if ( ! is_array( $resource ) ) {
+ continue;
+ }
+
+ $attributes = $resource;
+ if ( isset( $resource['href'] ) ) {
+ $href = $resource['href'];
+ if ( isset( $unique_resources[ $href ] ) ) {
+ continue;
+ }
+ $unique_resources[ $href ] = $attributes;
+ // Media can use imagesrcset and not href.
+ } elseif ( ( 'image' === $resource['as'] ) &&
+ ( isset( $resource['imagesrcset'] ) || isset( $resource['imagesizes'] ) )
+ ) {
+ if ( isset( $unique_resources[ $resource['imagesrcset'] ] ) ) {
+ continue;
+ }
+ $unique_resources[ $resource['imagesrcset'] ] = $attributes;
+ } else {
+ continue;
+ }
+ }
+
+ // Build and output the HTML for each unique resource.
+ foreach ( $unique_resources as $unique_resource ) {
+ $html = '';
+
+ foreach ( $unique_resource as $resource_key => $resource_value ) {
+ if ( ! is_scalar( $resource_value ) ) {
+ continue;
+ }
+
+ // Ignore non-supported attributes.
+ $non_supported_attributes = array( 'as', 'crossorigin', 'href', 'imagesrcset', 'imagesizes', 'type', 'media' );
+ if ( ! in_array( $resource_key, $non_supported_attributes, true ) && ! is_numeric( $resource_key ) ) {
+ continue;
+ }
+
+ // imagesrcset only usable when preloading image, ignore otherwise.
+ if ( ( 'imagesrcset' === $resource_key ) && ( ! isset( $unique_resource['as'] ) || ( 'image' !== $unique_resource['as'] ) ) ) {
+ continue;
+ }
+
+ // imagesizes only usable when preloading image and imagesrcset present, ignore otherwise.
+ if ( ( 'imagesizes' === $resource_key ) &&
+ ( ! isset( $unique_resource['as'] ) || ( 'image' !== $unique_resource['as'] ) || ! isset( $unique_resource['imagesrcset'] ) )
+ ) {
+ continue;
+ }
+
+ $resource_value = ( 'href' === $resource_key ) ? esc_url( $resource_value, array( 'http', 'https' ) ) : esc_attr( $resource_value );
+
+ if ( ! is_string( $resource_key ) ) {
+ $html .= " $resource_value";
+ } else {
+ $html .= " $resource_key='$resource_value'";
+ }
+ }
+ $html = trim( $html );
+
+ printf( "\n", $html );
+ }
+}
+
/**
* Retrieves a list of unique hosts of all enqueued scripts and styles.
*
diff --git a/tests/phpunit/tests/general/wpPreloadLinks.php b/tests/phpunit/tests/general/wpPreloadLinks.php
new file mode 100644
index 0000000000..d9f4150088
--- /dev/null
+++ b/tests/phpunit/tests/general/wpPreloadLinks.php
@@ -0,0 +1,253 @@
+assertSame( $expected, $actual );
+ }
+
+ /**
+ * Test provider for all preload link possible combinations.
+ *
+ * @return array[]
+ */
+ public function data_preload_resources() {
+ return array(
+ 'basic_preload' => array(
+ 'expected' => "\n",
+ 'urls' => array(
+ array(
+ 'href' => 'https://example.com/style.css',
+ 'as' => 'style',
+ ),
+ ),
+ ),
+ 'multiple_links' => array(
+ 'expected' => "\n" .
+ "\n",
+ 'urls' => array(
+ array(
+ 'href' => 'https://example.com/style.css',
+ 'as' => 'style',
+ ),
+ array(
+ 'href' => 'https://example.com/main.js',
+ 'as' => 'script',
+ ),
+ ),
+ ),
+ 'MIME_types' => array(
+ 'expected' => "\n" .
+ "\n" .
+ "\n",
+ 'urls' => array(
+ array(
+ // Should ignore not valid attributes.
+ 'not' => 'valid',
+ 'href' => 'https://example.com/style.css',
+ 'as' => 'style',
+ ),
+ array(
+ 'href' => 'https://example.com/video.mp4',
+ 'as' => 'video',
+ 'type' => 'video/mp4',
+ ),
+ array(
+ 'href' => 'https://example.com/main.js',
+ 'as' => 'script',
+ ),
+ ),
+ ),
+ 'CORS' => array(
+ 'expected' => "\n" .
+ "\n" .
+ "\n" .
+ "\n",
+ 'urls' => array(
+ array(
+ 'href' => 'https://example.com/style.css',
+ 'as' => 'style',
+ 'crossorigin' => 'anonymous',
+ ),
+ array(
+ 'href' => 'https://example.com/video.mp4',
+ 'as' => 'video',
+ 'type' => 'video/mp4',
+ ),
+ array(
+ 'href' => 'https://example.com/main.js',
+ 'as' => 'script',
+ ),
+ array(
+ // Should ignore not valid attributes.
+ 'ignore' => 'ignore',
+ 'href' => 'https://example.com/font.woff2',
+ 'as' => 'font',
+ 'type' => 'font/woff2',
+ 'crossorigin',
+ ),
+ ),
+ ),
+ 'media' => array(
+ 'expected' => "\n" .
+ "\n" .
+ "\n" .
+ "\n" .
+ "\n" .
+ "\n",
+ 'urls' => array(
+ array(
+ 'href' => 'https://example.com/style.css',
+ 'as' => 'style',
+ 'crossorigin' => 'anonymous',
+ ),
+ array(
+ 'href' => 'https://example.com/video.mp4',
+ 'as' => 'video',
+ 'type' => 'video/mp4',
+ ),
+ // Duplicated href should be ignored.
+ array(
+ 'href' => 'https://example.com/video.mp4',
+ 'as' => 'video',
+ 'type' => 'video/mp4',
+ ),
+ array(
+ 'href' => 'https://example.com/main.js',
+ 'as' => 'script',
+ ),
+ array(
+ 'href' => 'https://example.com/font.woff2',
+ 'as' => 'font',
+ 'type' => 'font/woff2',
+ 'crossorigin',
+ ),
+ array(
+ 'href' => 'https://example.com/image-narrow.png',
+ 'as' => 'image',
+ 'media' => '(max-width: 600px)',
+ ),
+ array(
+ 'href' => 'https://example.com/image-wide.png',
+ 'as' => 'image',
+ 'media' => '(min-width: 601px)',
+ ),
+
+ ),
+ ),
+ 'media_extra_attributes' => array(
+ 'expected' => "\n" .
+ "\n" .
+ "\n" .
+ "\n" .
+ "\n" .
+ "\n" .
+ "\n" .
+ "\n",
+ 'urls' => array(
+ array(
+ 'href' => 'https://example.com/style.css',
+ 'as' => 'style',
+ 'crossorigin' => 'anonymous',
+ ),
+ array(
+ 'href' => 'https://example.com/video.mp4',
+ 'as' => 'video',
+ 'type' => 'video/mp4',
+ ),
+ array(
+ 'href' => 'https://example.com/main.js',
+ 'as' => 'script',
+ ),
+ array(
+ 'href' => 'https://example.com/font.woff2',
+ 'as' => 'font',
+ 'type' => 'font/woff2',
+ 'crossorigin',
+ ),
+ // imagesrcset only possible when using image, ignore.
+ array(
+ 'href' => 'https://example.com/font.woff2',
+ 'as' => 'font',
+ 'type' => 'font/woff2',
+ 'imagesrcset' => '640.png 640w, 800.png 800w, 1024.png 1024w',
+ ),
+ // imagesizes only possible when using image, ignore.
+ array(
+ 'href' => 'https://example.com/font.woff2',
+ 'as' => 'font',
+ 'type' => 'font/woff2',
+ 'imagesizes' => '100vw',
+ ),
+ // Duplicated href should be ignored.
+ array(
+ 'href' => 'https://example.com/font.woff2',
+ 'as' => 'font',
+ 'type' => 'font/woff2',
+ 'crossorigin',
+ ),
+ array(
+ 'href' => 'https://example.com/image-640.png',
+ 'as' => 'image',
+ 'imagesrcset' => '640.png 640w, 800.png 800w, 1024.png 1024w',
+ 'imagesizes' => '100vw',
+ ),
+ // Omit href so that unsupporting browsers won't request a useless image.
+ array(
+ 'as' => 'image',
+ 'imagesrcset' => '640.png 640w, 800.png 800w, 1024.png 1024w',
+ 'imagesizes' => '100vw',
+ ),
+ // Duplicated imagesrcset should be ignored.
+ array(
+ 'as' => 'image',
+ 'imagesrcset' => '640.png 640w, 800.png 800w, 1024.png 1024w',
+ 'imagesizes' => '100vw',
+ ),
+ array(
+ 'href' => 'https://example.com/image-wide.png',
+ 'as' => 'image',
+ 'media' => '(min-width: 601px)',
+ ),
+ // No href but not imagesrcset, should be ignored.
+ array(
+ 'as' => 'image',
+ 'media' => '(min-width: 601px)',
+ ),
+ // imagesizes is optional.
+ array(
+ 'href' => 'https://example.com/image-800.png',
+ 'as' => 'image',
+ 'imagesrcset' => '640.png 640w, 800.png 800w, 1024.png 1024w',
+ ),
+ // imagesizes should be ignored since imagesrcset not present.
+ array(
+ 'href' => 'https://example.com/image-640.png',
+ 'as' => 'image',
+ 'imagesizes' => '100vw',
+ ),
+ ),
+ ),
+ );
+ }
+
+}