diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php
index c8643749fb..06a973b0bb 100644
--- a/src/wp-admin/includes/admin-filters.php
+++ b/src/wp-admin/includes/admin-filters.php
@@ -43,6 +43,9 @@ add_action( 'admin_head', 'wp_color_scheme_settings' );
add_action( 'admin_head', 'wp_site_icon' );
add_action( 'admin_head', '_ipad_meta' );
+// Prerendering.
+add_filter( 'admin_head', 'wp_resource_hints' );
+
add_action( 'admin_print_scripts-post.php', 'wp_page_reload_on_back_button_js' );
add_action( 'admin_print_scripts-post-new.php', 'wp_page_reload_on_back_button_js' );
diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
index 70da2f2e2a..fe716165fa 100644
--- a/src/wp-includes/default-filters.php
+++ b/src/wp-includes/default-filters.php
@@ -239,6 +239,7 @@ add_action( 'wp_head', 'wp_print_styles', 8 );
add_action( 'wp_head', 'wp_print_head_scripts', 9 );
add_action( 'wp_head', 'wp_generator' );
add_action( 'wp_head', 'rel_canonical' );
+add_action( 'wp_head', 'wp_resource_hints' );
add_action( 'wp_head', 'wp_shortlink_wp_head', 10, 0 );
add_action( 'wp_head', 'wp_site_icon', 99 );
add_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php
index db0dfafa74..222f248252 100644
--- a/src/wp-includes/general-template.php
+++ b/src/wp-includes/general-template.php
@@ -2787,6 +2787,97 @@ function wp_site_icon() {
}
}
+/**
+ * Prints resource hints to browsers for pre-fetching, pre-rendering and pre-connecting to web sites.
+ *
+ * Gives hints to browsers to prefetch specific pages or render them in the background,
+ * to perform DNS lookups or to begin the connection handshake (DNS, TCP, TLS) in the background.
+ *
+ * These performance improving indicators work by using ``.
+ *
+ * @since 4.6.0
+ */
+function wp_resource_hints() {
+ $hints = array(
+ 'dns-prefetch' => wp_resource_hints_scripts_styles(),
+ 'preconnect' => array( 's.w.org' ),
+ 'prefetch' => array(),
+ 'prerender' => array(),
+ );
+
+ foreach ( $hints as $relation_type => $urls ) {
+ /**
+ * Filters domains and URLs for resource hints.
+ *
+ * @since 4.6.0
+ *
+ * @param array $urls URLs to print for resource hints.
+ * @param string $relation_type The relation type the URLs are printed for, e.g. 'preconnect' or 'prerender'.
+ */
+ $urls = apply_filters( 'wp_resource_hints', $urls, $relation_type );
+ $urls = array_unique( $urls );
+
+ foreach ( $urls as $url ) {
+ $url = esc_url( $url, array( 'http', 'https' ) );
+
+ if ( in_array( $relation_type, array( 'preconnect', 'dns-prefetch' ) ) ) {
+ $parsed = parse_url( $url );
+
+ if ( ! empty( $parsed['scheme'] ) ) {
+ $url = $parsed['scheme'] . '://' . $parsed['host'];
+ } else {
+ $url = $parsed['host'];
+ }
+ }
+
+ printf( "\r\n", $relation_type, $url );
+ }
+ }
+}
+
+/**
+ * Adds dns-prefetch for all scripts and styles enqueued from external hosts.
+ *
+ * @since 4.6.0
+ */
+function wp_resource_hints_scripts_styles() {
+ global $wp_scripts, $wp_styles;
+
+ $unique_hosts = array();
+
+ if ( is_object( $wp_scripts ) && ! empty( $wp_scripts->registered ) ) {
+ foreach ( $wp_scripts->registered as $registered_script ) {
+ $src = $registered_script->src;
+ // Make sure the URL has a scheme, otherwise parse_url() could fail to pass the host.
+ if ( '//' == substr( $src, 0, 2 ) ) {
+ $src = set_url_scheme( $src );
+ }
+
+ $this_host = parse_url( $src, PHP_URL_HOST );
+ if ( ! empty( $this_host ) && ! in_array( $this_host, $unique_hosts ) && $this_host !== $_SERVER['SERVER_NAME'] ) {
+ $unique_hosts[] = $this_host;
+ }
+ }
+ }
+
+ if ( is_object( $wp_styles ) && ! empty( $wp_styles->registered ) ) {
+ foreach ( $wp_styles->registered as $registered_style ) {
+ $src = $registered_style->src;
+ // Make sure the URL has a scheme, otherwise parse_url() could fail to pass the host.
+ if ( '//' == substr( $src, 0, 2 ) ) {
+ $src = set_url_scheme( $src );
+ }
+
+ $this_host = parse_url( $src, PHP_URL_HOST );
+ if ( ! empty( $this_host ) && ! in_array( $this_host, $unique_hosts ) && $this_host !== $_SERVER['SERVER_NAME'] ) {
+ $unique_hosts[] = $this_host;
+ }
+ }
+ }
+
+ return $unique_hosts;
+}
+
/**
* Whether the user should have a WYSIWIG editor.
*
diff --git a/tests/phpunit/tests/general/resourceHints.php b/tests/phpunit/tests/general/resourceHints.php
new file mode 100644
index 0000000000..5f0dc354d1
--- /dev/null
+++ b/tests/phpunit/tests/general/resourceHints.php
@@ -0,0 +1,152 @@
+old_wp_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null;
+ $this->old_wp_styles = isset( $GLOBALS['wp_styles'] ) ? $GLOBALS['wp_styles'] : null;
+
+ remove_action( 'wp_default_scripts', 'wp_default_scripts' );
+ remove_action( 'wp_default_styles', 'wp_default_styles' );
+
+ $GLOBALS['wp_scripts'] = new WP_Scripts();
+ $GLOBALS['wp_scripts']->default_version = get_bloginfo( 'version' );
+ $GLOBALS['wp_styles'] = new WP_Styles();
+ $GLOBALS['wp_styles']->default_version = get_bloginfo( 'version' );
+ }
+
+ function tearDown() {
+ $GLOBALS['wp_scripts'] = $this->old_wp_scripts;
+ $GLOBALS['wp_styles'] = $this->old_wp_styles;
+ add_action( 'wp_default_scripts', 'wp_default_scripts' );
+ add_action( 'wp_default_styles', 'wp_default_styles' );
+ parent::tearDown();
+ }
+
+ function test_should_have_defaults_on_frontend() {
+ $expected = "\r\n";
+
+ $this->expectOutputString( $expected );
+
+ wp_resource_hints();
+ }
+
+ function test_dns_prefetching() {
+ $expected = "\r\n" .
+ "\r\n" .
+ "\r\n" .
+ "\r\n";
+
+ add_filter( 'wp_resource_hints', array( $this, '_add_dns_prefetch_domains' ), 10, 2 );
+
+ $actual = get_echo( 'wp_resource_hints' );
+
+ remove_filter( 'wp_resource_hints', array( $this, '_add_dns_prefetch_domains' ) );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+ function _add_dns_prefetch_domains( $hints, $method ) {
+ if ( 'dns-prefetch' === $method ) {
+ $hints[] = 'http://wordpress.org';
+ $hints[] = 'https://google.com';
+ $hints[] = '//make.wordpress.org';
+ }
+
+ return $hints;
+ }
+
+ function test_prerender() {
+ $expected = "\r\n" .
+ "\r\n" .
+ "\r\n" .
+ "\r\n";
+
+ add_filter( 'wp_resource_hints', array( $this, '_add_prerender_urls' ), 10, 2 );
+
+ $actual = get_echo( 'wp_resource_hints' );
+
+ remove_filter( 'wp_resource_hints', array( $this, '_add_prerender_urls' ) );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+ function _add_prerender_urls( $hints, $method ) {
+ if ( 'prerender' === $method ) {
+ $hints[] = 'https://make.wordpress.org/great-again';
+ $hints[] = 'http://jobs.wordpress.net';
+ $hints[] = '//core.trac.wordpress.org';
+ }
+
+ return $hints;
+ }
+
+ function test_parse_url_dns_prefetch() {
+ $expected = "\r\n" .
+ "\r\n";
+
+ add_filter( 'wp_resource_hints', array( $this, '_add_dns_prefetch_long_urls' ), 10, 2 );
+
+ $actual = get_echo( 'wp_resource_hints' );
+
+ remove_filter( 'wp_resource_hints', array( $this, '_add_dns_prefetch_long_urls' ) );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+ function _add_dns_prefetch_long_urls( $hints, $method ) {
+ if ( 'dns-prefetch' === $method ) {
+ $hints[] = 'http://make.wordpress.org/wp-includes/css/editor.css';
+ }
+
+ return $hints;
+ }
+
+ /**
+ * @group foo
+ */
+ function test_dns_prefetch_styles() {
+ $expected = "\r\n" .
+ "\r\n";
+
+ $args = array(
+ 'family' => 'Open+Sans:400',
+ 'subset' => 'latin',
+ );
+
+ wp_enqueue_style( 'googlefonts', add_query_arg( $args, '//fonts.googleapis.com/css' ) );
+
+ $actual = get_echo( 'wp_resource_hints' );
+
+ wp_dequeue_style( 'googlefonts' );
+
+ $this->assertEquals( $expected, $actual );
+
+ }
+
+ function test_dns_prefetch_scripts() {
+ $expected = "\r\n" .
+ "\r\n";
+
+ $args = array(
+ 'family' => 'Open+Sans:400',
+ 'subset' => 'latin',
+ );
+
+ wp_enqueue_script( 'googlefonts', add_query_arg( $args, '//fonts.googleapis.com/css' ) );
+
+ $actual = get_echo( 'wp_resource_hints' );
+
+ wp_dequeue_style( 'googlefonts' );
+
+ $this->assertEquals( $expected, $actual );
+ }
+
+}