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()
.
Commentaires
Commentaire de richnou.
Technique qui semble intéressante…
Par contre, petite question: si le gain de chargement n’est que de quelques milisecondes, ajouter la fonction avec ses multiples tests prend également du temps (peu certes). N’est ce pas alors ajouter beaucoup pour un bénéfice minime ? Avez vous un moyen de mesurer ?
Commentaire de Greg.
Difficile à dire quant aux moyens de mesurer, et effectivement la génération du code va quelque peu empiéter sur le gain réalisé, mais c’est infime (le script n’est pas violent et les options demandées sont déjà pré-chargées par WordPress). Pour comparer il faudrait mesurer le gain au niveau de la résolution des DNS et lui retrancher le temps d’exécution du script, ça me semble faisable mais difficilement précis. Et bien sûr, plus on aura de ressources externes, et plus le gain sera important.
Là j’ai montré un script qui s’adapte à pas mal de configurations mais la meilleure solution serait d’écrire les balises en dur directement dans le head pour une efficacité accrue, ce qui est faisable dans la majorité des cas puisqu’on ne va pas changer notre site tous les 4 matins. Ou solution intermédiaire, modifier le script pour ne garder que ce qui est valable pour son propre site (on n’a pas forcément une option pour Analytics dans son thème par exemple).
A la limite, là où le code présenté serait le plus bénéfique c’est pour pour un thème que l’on créé et que l’on compte mettre à disposition.
Merci pour ta réaction richnou :)
Commentaire de Daniel.
Ouah, excellent cet article !
Pour le gain, je ne serais pas si modeste. Sur une connexion desktop, je suis d’accord sur le gain faible. Mais sur mobile, ce sera extra car une connexion mobile va avoir des temps de latence environ 5 à 10 fois supérieur pour chaque résolution DNS. Donc ce sera un véritable gain dans ce cas de figure.
Commentaire de Greg.
C’est vrai que j’oublie toujours les mobiles sur ce point particulier, merci pour le rappel :)
Commentaire de Arnaud.
Ah sympa cette astuce !
Je ne connaissais pas, à voir dans les faits si le gain de temps est vraiment appréciable mais en tout cas ça n’a pas l’air compliqué à mettre en place.
Commentaire de Greg.
Mise à jour : Correction de bug : ajout de
array_values()
aprèsarray_unique()
.Commentaire de Fabien.
Merci excellent article !
Par contre l’implémentation n’étant pas si simple je vais attendre un peu avant de m’y coller ;)