Contenu principal
Fetch

Utiliser le DNS Prefetch avec WordPress

Si sur votre site vous utilisez des ressources externes (images, scripts, styles, etc, venant d’un autre domaine que celui de votre site), le DNS Prefetch peut vous être utile pour accélérer un peu l’affichage de vos pages. Voyons quelques exemples d’application dans WordPress. En fait j’ai découvert le DNS Prefetch assez récemment, en parcourant la documentation de HTML5 Boilerplate. Mais qu’est-ce que c’est ?

Si vous savez ce que sont les DNS, le nom de DNS Prefetch parle de lui-même. Dans le cas contraire, et pour faire court, si par exemple vous utilisez sur votre site une version de jQuery hébergée par Google, le navigateur doit contacter le domaine ajax.googleapis.com. Mais à chaque domaine correspond une adresse IP, celle du serveur sur lequel sont hébergés les fichiers, et c’est justement ce qui va intéresser le navigateur. Trouver une adresse IP en fonction d’un nom de domaine c’est ce qu’on appelle la résolution de DNS, et ça prend une poignée de millisecondes. C’est peu, me direz-vous, mais le tout cumulé, ça ne fait pas de mal de grappiller un peu.

Le DNS Prefetch permet de résoudre le DNS avant de rencontrer la ressource en question, et donc de gagner un peu de temps au moment de la télécharger. Concrètement ça se traduit par l’ajout de balises <link/> dans le head du site, et le plus tôt possible dans le code.
Ça donnera ceci au final :

Pour un peu plus d’infos et de documentation, voir la documentation de HTML5 Boilerplate sur GitHub.

Trouver les url à précharger

La première étape consistera à recenser ces url, et on peut parfois avoir des surprises. Tout d’abord il nous faut créer une fonction que l’on va hooker dans le head. Le code est à mettre dans le fichier functions.php de votre thème, comme d’habitude.

12345

add_action( 'wp_head', 'sf_dns_prefetch', 0 );
function sf_dns_prefetch() {
	$dns_prefetch = array();
	// ...
}

Le hook est lancé le plus tôt possible grâce au « 0 », mais à vous d’adapter selon votre thème. On créé une variable array $dns_prefetch dans laquelle on va ranger nos url. Bien sûr on peut aussi écrire les balises directement dans le thème sans créer de fonction php pour ça aussi. Là cela permettra au thème de s’adapter si vos ressources externes changent, ou d’ajouter des conditions selon les pages par exemple.

Premier exemple : un thème avec une option pour Google Analytics. Pour cela il vous faudra connaitre l’option à aller chercher, ce que je ne peux pas deviner pour vous ;)

123456789

add_action( 'wp_head', 'sf_dns_prefetch', 0 );
function sf_dns_prefetch() {
	$dns_prefetch = array();
	
	$theme_options	= get_option( 'options_de_mon_theme_qui_rox' );
	if ( isset($theme_options['analytics']) && $theme_options['analytics'] )
		$dns_prefetch[]	= '//ssl.google-analytics.com';
	// ...
}

Vous remarquerez qu’on enlève les http: ou https: au début de l’url, en revanche il faut garder les « // » ainsi que les sous-domaines (www aussi). Mais bon, à ce stade c’est pas grave si on les laisse car on s’en occupera plus tard par sécurité.

Deuxième exemple : vos médias sont hébergées sur un CDN. On va aller chercher la valeur du champ dans l’administration où vous indiquez l’url du CDN. Ensuite on la compare à l’url de votre site et si elles sont différentes, on l’ajoute.

01020304050607080910111213

add_action( 'wp_head', 'sf_dns_prefetch', 0 );
function sf_dns_prefetch() {
	$dns_prefetch = array();
	
	$theme_options	= get_option( 'options_de_mon_theme_qui_rox' );
	if ( isset($theme_options['analytics']) && $theme_options['analytics'] )
		$dns_prefetch[]	= '//ssl.google-analytics.com';
	
	$upload_url_path = get_option('upload_url_path');
	if ( $upload_url_path && strpos($upload_url_path, site_url('/')) !== 0 )
		$dns_prefetch[]	= $upload_url_path;
	// ...
}

Troisième exemple : Gravatar.
Ici nous allons tester si :
– l’option des avatars est activée, et,
– l’admin bar est affichée ou,
– les commentaires sont ouverts et nous sommes sur une page « singular ».
Ces conditions sont à modifier selon votre thème.

010203040506070809101112131415161718

add_action( 'wp_head', 'sf_dns_prefetch', 0 );
function sf_dns_prefetch() {
	$dns_prefetch = array();
	
	$theme_options	= get_option( 'options_de_mon_theme_qui_rox' );
	if ( isset($theme_options['analytics']) && $theme_options['analytics'] )
		$dns_prefetch[]	= '//ssl.google-analytics.com';
	
	$upload_url_path = get_option('upload_url_path');
	if ( $upload_url_path && strpos($upload_url_path, site_url('/')) !== 0 )
		$dns_prefetch[]	= $upload_url_path;

	if ( get_option('show_avatars') && ( is_admin_bar_showing() || ( is_singular() && comments_open() ) ) ) {
		$dns_prefetch[]	= '//secure.gravatar.com';
		$dns_prefetch[]	= '//0.gravatar.com';
	}
	// ...
}

Javascript et CSS
C’est le plus gros morceau. On va utiliser les variables globales $wp_scripts et $wp_styles (qui sont construites de la même manière), elles contiennent les scripts/styles enregistrés (register) et en file d’attente (enqueue). On va ensuite boucler sur ces ressources pour tester l’url. Les url sont stockées de deux façons : en relatif (ce sont les ressources locales de WordPress, elles commencent par /wp-admin/ ou /wp-includes/) ou en absolu (ce sont les fichiers externes, ou ceux enregistrés depuis le thème ou un plugin).
Je vous balance le code et je vous explique quelques détails ensuite :)

0102030405060708091011121314151617181920212223242526272829303132333435

add_action( 'wp_head', 'sf_dns_prefetch', 0 );
function sf_dns_prefetch() {
	global $wp_scripts, $wp_styles;
	$dns_prefetch = array();
	
	$theme_options	= get_option( 'options_de_mon_theme_qui_rox' );
	if ( isset($theme_options['analytics']) && $theme_options['analytics'] )
		$dns_prefetch[]	= '//ssl.google-analytics.com';
	
	$upload_url_path = get_option('upload_url_path');
	if ( $upload_url_path && strpos($upload_url_path, site_url('/')) !== 0 )
		$dns_prefetch[]	= $upload_url_path;

	if ( get_option('show_avatars') && ( is_admin_bar_showing() || ( is_singular() && comments_open() ) ) ) {
		$dns_prefetch[]	= '//secure.gravatar.com';
		$dns_prefetch[]	= '//0.gravatar.com';
	}

	$scripts = array( $wp_styles, $wp_scripts );
	foreach ( $scripts as $wp_files ) {
		if ( count($wp_files->queue) ) {
			$dirs = array( '/wp-admin/', '/wp-includ' );
			foreach ( $wp_files->queue as $handle ) {
				if ( !isset($wp_files->registered[$handle]) )
					continue;
				$wp_file = $wp_files->registered[$handle];
				if ( strpos( $wp_file->src, $wp_files->base_url ) !== 0 && !in_array( substr($wp_file->src, 0, 10), $dirs ) )
					$dns_prefetch[]	= '//'.reset( explode( '/', str_replace( array('https://', 'http://'), '', $wp_file->src ) ) );
			}
			unset($dirs, $wp_file, $handle);
		}
	}
	unset($scripts, $wp_files);
	// ...
}

L’idée est de boucler sur les fichiers en file d’attente ($wp_files->queue) car la liste sera beaucoup (beaucoup !) moins longue que celle des fichiers enregistrés ($wp_files->registered). Ensuite on récupère les propriétés du fichier dans la liste des fichiers enregistrés (dont l’url) avec $wp_file = $wp_files->registered[$handle];. Si l’url ne commence pas par celle de votre site, c’est bon ($wp_files->base_url contient l’url de base des fichiers, l’url du site donc) (là il s’agirait d’un fichier du thème ou d’un plugin). Ensuite on teste les 10 premiers caractères du l’url pour vérifier qu’ils ne commencent pas par ‘/wp-admin/’ ou ‘/wp-includ’ (oui, ‘/wp-includ’ sans le e à la fin, les 10 premiers caractères on vous dit !), ça voudrait dire que ce sont des fichiers de WordPress dont l’url est relative (!in_array( substr($wp_file->src, 0, 10), $dirs )).
Si ces deux conditions sont bonnes, alors on ajoute l’url à notre tableau, en ne gardant que la partie qui nous intéresse.

Deux inconvénients à cette méthode :
– si un script est mis en file d’attente à mi-page, on ne pourra pas créer la balise de prefetch dans le head, car le script n’est pas encore dans « queue ».
– il faut que les scripts/styles soient effectivement dans « queue », ce qui n’est pas obligatoire à cause des dépendances.
Ceci marchera :

12

wp_enqueue_script('jquery');
wp_enqueue_script('machin', 'http://cdn.machin.com/script.js', array('jquery'));

« machin » a besoin de jQuery, les deux sont mis en file d’attente, tout va bien.
Ceci marche aussi :

1

wp_enqueue_script('machin', 'http://cdn.machin.com/script.js', array('jquery'));

« machin » a besoin de jQuery, mais grâce à la dépendance on n’a pas besoin de mettre jQuery en file d’attente, ce sera fait automatiquement (si jQuery est enregistré). Ça marche, oui, mais dans ce cas là, jQuery n’apparait pas dans « queue », donc on ne bouclera pas dessus, et si on utilise une version distante…
Bref, il faut veiller à ce que tous les scripts dont nous avons besoin soient effectivement en file d’attente, ce qui en général est le cas (sauf quand comme moi on veut s’économiser une ligne de code).

Dernier petit détail concernant cette partie : ajouter un filtre (pour ajouter des url facilement plus tard) et supprimer les doublons.

01020304050607080910111213141516171819202122232425262728293031323334353637

add_action( 'wp_head', 'sf_dns_prefetch', 0 );
function sf_dns_prefetch() {
	global $wp_scripts, $wp_styles;
	$dns_prefetch = array();
	
	$theme_options	= get_option( 'options_de_mon_theme_qui_rox' );
	if ( isset($theme_options['analytics']) && $theme_options['analytics'] )
		$dns_prefetch[]	= '//ssl.google-analytics.com';
	
	$upload_url_path = get_option('upload_url_path');
	if ( $upload_url_path && strpos($upload_url_path, site_url('/')) !== 0 )
		$dns_prefetch[]	= $upload_url_path;

	if ( get_option('show_avatars') && ( is_admin_bar_showing() || ( is_singular() && comments_open() ) ) ) {
		$dns_prefetch[]	= '//secure.gravatar.com';
		$dns_prefetch[]	= '//0.gravatar.com';
	}

	$scripts = array( $wp_styles, $wp_scripts );
	foreach ( $scripts as $wp_files ) {
		if ( count($wp_files->queue) ) {
			$dirs = array( '/wp-admin/', '/wp-includ' );
			foreach ( $wp_files->queue as $handle ) {
				if ( !isset($wp_files->registered[$handle]) )
					continue;
				$wp_file = $wp_files->registered[$handle];
				if ( strpos( $wp_file->src, $wp_files->base_url ) !== 0 && !in_array( substr($wp_file->src, 0, 10), $dirs ) )
					$dns_prefetch[]	= '//'.reset( explode( '/', str_replace( array('https://', 'http://'), '', $wp_file->src ) ) );
			}
			unset($dirs, $wp_file, $handle);
		}
	}
	unset($scripts, $wp_files);

	$dns_prefetch = array_values( array_unique( apply_filters('dns_prefetch', $dns_prefetch) ) );
	// ...
}

Hey ! On a fini cette partie !

Créer les balises

Notre tableau est rempli, il ne reste plus qu’à les imprimer dans le head. Une simple boucle for suffit.

01020304050607080910111213141516171819202122232425262728293031323334353637383940414243

add_action( 'wp_head', 'sf_dns_prefetch', 0 );
function sf_dns_prefetch() {
	global $wp_scripts, $wp_styles;
	$dns_prefetch = array();
	
	$theme_options	= get_option( 'options_de_mon_theme_qui_rox' );
	if ( isset($theme_options['analytics']) && $theme_options['analytics'] )
		$dns_prefetch[]	= '//ssl.google-analytics.com';
	
	$upload_url_path = get_option('upload_url_path');
	if ( $upload_url_path && strpos($upload_url_path, site_url('/')) !== 0 )
		$dns_prefetch[]	= $upload_url_path;

	if ( get_option('show_avatars') && ( is_admin_bar_showing() || ( is_singular() && comments_open() ) ) ) {
		$dns_prefetch[]	= '//secure.gravatar.com';
		$dns_prefetch[]	= '//0.gravatar.com';
	}

	$scripts = array( $wp_styles, $wp_scripts );
	foreach ( $scripts as $wp_files ) {
		if ( count($wp_files->queue) ) {
			$dirs = array( '/wp-admin/', '/wp-includ' );
			foreach ( $wp_files->queue as $handle ) {
				if ( !isset($wp_files->registered[$handle]) )
					continue;
				$wp_file = $wp_files->registered[$handle];
				if ( strpos( $wp_file->src, $wp_files->base_url ) !== 0 && !in_array( substr($wp_file->src, 0, 10), $dirs ) )
					$dns_prefetch[]	= '//'.reset( explode( '/', str_replace( array('https://', 'http://'), '', $wp_file->src ) ) );
			}
			unset($dirs, $wp_file, $handle);
		}
	}
	unset($scripts, $wp_files);

	$dns_prefetch = array_values( array_unique( apply_filters('dns_prefetch', $dns_prefetch) ) );

	if ( $dns_prefetch_count = count($dns_prefetch) ) {	// C'est bien un simple "=", pas un double
		for ( $i=0; $i < $dns_prefetch_count; ++$i ) {
			echo "\n\t\t".'';
		}
	}
	unset($dns_prefetch_count, $dns_prefetch);
}

Dernière étape

Ouvrir une bière et se féliciter d’avoir aussi bien travaillé.

NOTA : pour enlever les « http » et « https » j’ai fait au plus simple avec str_replace, dans 99% des cas ça ne posera pas de problème. Vérifiez de ne pas être le boulet 1% qui reste :D

Concrètement

Le gain ? Pas énorme dans la plupart des cas je pense, quelques centaines de millisecondes tout au plus, mais c’est toujours bon à prendre (vous pourrez voir ça dans l’onglet Réseau de Firebug ou je-sais-pas-où dans la console développement de Chrome). EDIT : Dans les commentaires, Daniel me rappelle à juste titre que le gain n’est pas négligeable sur mobile.
Support navigateurs : Chrome (anciennement appelée « prerender »), Firefox 3.5+, Safari 5+, Opera (version inconnue), IE 9 (appelée « Pre-resolution » sur blogs.msdn.com, mais osef de IE de toute façon).

Un petit conseil pour la route : utiliser des CDN c’est bien, en abuser ça craint.

See ya !

[update] Correction : remplacement d’une constante WP_SITE_URL (qui n’existe pas nativement dans WordPress) par site_url('/').
[update] Correction de bug : ajout de array_values() après array_unique().