Combo Handler : Optimiser le nombre de requêtes HTTP pour les fichiers CSS et JavaScript

5

Posted by Will | Posted on 19-06-2009

 Performance  Cache  Combo Handler  Yahoo  CSS  JavaScript  Optimisation

YSlowAprès avoir regardé une excellente conférence d'Eric Daspet sur l'amélioration des performances d'un site web j'ai appliqué les conseils et bonnes pratiques (Yahoo notamment) sur mon propre site. Résultat assez satisfaisant mais encore perfectible, je suis en grade B ou A si je désactive les appels FeedBurner et Twitter (très longs).

Pour commencer, j'ai passé un coup de smush.it sur mes images qui étaient déjà bien optimisées. Puis j'ai configuré mon cache Apache et les temps d'expiration. J'ai déporté le JavaScript qui n'était pas déjà en bas de page. J'ai configuré les ETags et activé la compression gzip. Pas de minimisation des CSS et JavaScripts, pas de gain de temps grâce à ce que vais présenter ensuite. Puis pour terminer j'ai commencé à optimiser le nombre de requêtes HTTP.

Pour cela j'ai appliqué la technique du combo handler de Yahoo.

 

Combo Handler c'est une technique initiée par Yahoo qui permet de réduire le nombre de requêtes HTTP pour nos fichiers CSS et JavaScript.

Le principe est de concaténer les différents fichiers en un et de l'envoyer en une fois au navigateur. J'ai donc repris ce principe en PHP, forcer l'utilisation du cache et activer la compression. Gain en performances garantie !

Voilà le script pour les fichiers JavaScript :

<?php

ob_start('ob_gzhandler');

header('Cache-Control: public;max-age=604800');
header('Expires: Thu, 15 Apr 2010 20:00:00 GMT');
header('Content-Type: text/javascript');

if(!empty($_GET['files']))
{
	$js_files = explode('|', $_GET['files']);

	foreach($js_files as $js)
	{
		if(isValid($js))
		{
			readfile('../' . $js);
			echo "\n";
		}
	}
}

function isValid($file)
{
	return substr($file, -3) == '.js';
}

?>

 

Je commence par activer la compression grâce à :

ob_start('ob_gzhandler');

 

Puis je configure les entêtes HTTP, d'abord le cache puis le type de contenu soumis.

header('Cache-Control: public;max-age=604800');
header('Expires: Thu, 15 Apr 2010 20:00:00 GMT');
header('Content-Type: text/javascript');

 

Pour chaque fichier en paramètre j'envoie son contenu dans la sortie standard avec la fonction readfile().

 

Le même script pour les CSS :

<?php

ob_start('ob_gzhandler');

header('Cache-Control: public;max-age=604800');
header('Expires: Thu, 15 Apr 2010 20:00:00 GMT');
header('Content-Type: text/css');

if(!empty($_GET['files']))
{
	$css_files = explode('|', $_GET['files']);

	foreach($css_files as $css)
	{
		if(isValid($css))
		{
			readfile('../' . $css);
			echo "\n";
		}
	}
}

function isValid($file)
{
	return substr($file, -4) == '.css';
}

?>

Le principe est le même.

 

L'appel dans le code HTML est le suivant :

<link rel="stylesheet" type="text/css" href="http://www.willdurand.fr/cache/combo_css.php?files=css/fixIE.css|css/style.css|css/pager.css|css/form_edit.css|css/form_edit_post.css|css/mod_edito.css|css/mod_search.css|css/mod_archive.css|css/mod_popular_posts.css|css/lightbox.css|css/user.css|css/mod_stay_in_touch.css|css/twitter.css|css/syntaxHighlighter/shCore.css|css/syntaxHighlighter/shThemeDefault.css|" media="screen,projection,print" />

et

<script type="text/javascript" src="http://www.willdurand.fr/cache/combo_js.php?files=js/mt.js|js/scriptaculous/prototype.js|js/scriptaculous/scriptaculous.js|js/scriptaculous/effects.js|js/scriptaculous/builder.js|js/fckeditor/fckeditor.js|js/load_wysiwyg.js|js/scriptaculous/controls.js|js/autocomplete.js|js/swfobject.js|js/lightbox.js|js/syntaxHighlighter/shCore.js|js/syntaxHighlighter/shBrushJScript.js|js/syntaxHighlighter/shBrushJava.js|js/syntaxHighlighter/shBrushPhp.js|js/syntaxHighlighter/shBrushBash.js|js/syntaxHighlighter/shBrushXml.js|js/syntaxHighlighter/shBrushYml.js|js/blogger.js"></script>

 

Dans Firebug on peut voir la réponse suivante (pour le CSS) :

html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,
acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,
strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,
table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;
font-size:100%;vertical-align:baseline;background:transparent}
/* ------------------------------
HTML Redefine Tags
------------------------------ */
body{
font-family: "Lucida Grande", Arial, Helvetica, Georgia, sans-serif;
margin: 0;
padding: 0;
background-image: url(../images/header_bg.png);
background-repeat: repeat-x;
background-position:  top center;
background-color: #343434;
vertical-align: top;
}
input, textarea{
margin:0; 
padding:0;
border: 1px solid #CCCCCC;
}
h1, h2, h3, h4, h5, h6{
font-family: "Trebuchet MS",Helvetica,sans-serif;
font-weight: normal;
margin-top: 10px;
margin-right: 0pt;
margin-bottom: 0px;
margin-left: 0pt;
color: #000;
}
a:link, a:visited{
text-decoration: none;
color: #494949;
}
a:hover{
text-decoration: underline;
color: #494949;
}
/* ------------------------------
PAGE STRUCTURE
------------------------------ */
#container{
width:1000px; 
margin:.5% auto;
}
#topbar{
width:auto; 
display:block; 
height:140px;
}
#header{
width:800px;
float:left;
}
#main{
width:auto; 
display:block; 
padding:10px 0;
background-image: url(../images/bg_page.png);
background-repeat: repeat-y;
}
#bas_content {
width:auto; 
height:26px;
display:block; 
background-image: url(../images/footer.png);
}
#column_left{
width:710px;
float:left;
}
#column_right{
padding:25px;
width:240px;
float:left;
}
#menu_bas {
margin-right:40px;
padding-top:20px;
text-align:right;
}
#footer{
width:auto; 
display:block; 
padding:10px 0; 
font-size:11px; 
color:#666666;
text-align:center;
padding-bottom:22px;
}

#rss {
height:140px;
text-align:center;
}
#rss a{
margin:30px;
width:64px;
height:64px;
background-image: url(../images/rss_feed.png);
background-position:top;
float:left;
}
#rss a:hover{
background-position:bottom;
text-decoration: underline;
}
#menu_bas a{
padding:2px 20px 0 0; 
height:16px; 
background:url(../images/arrow.png) no-repeat center right;
font-size: 0.8em;
font-weight: bold;
color: #B9B9B9;
text-decoration: none;
}
#menu_bas a:hover{
text-decoration: underline;
}
div.spacer{
clear:both; 
height:10px; 
display:block;
}
/* ------------------------------
Errors / Msgs / 404
------------------------------ */
#error404 h3, #column_right h3{
font-size: 1.5em;
margin-top: 5px;
margin-right: 0pt;
padding-top:10px;
margin-bottom: 10px;
margin-left: 0pt;
border-top-width: 1px;
border-top-style: solid;
border-top-color: #828282;
color:#494949;
}
.msg_message {
color:green;
font-size:20px;
text-align:center;
border:2px solid green;
margin:20px;
padding:20px;
}
.msg_error {
color:red;
font-size:20px;
text-align:center;
border:2px solid red;
margin:20px;
padding:20px;
}
.msg_error p {
text-align:left;
margin-left:50px;
padding:0 0 0 20px;
background:url(../images/error.png) no-repeat center left;
}
.error404 div {
color:#343434;
text-align:center;
font-size:24px;
margin-bottom:20px;
}
.error404 p {
width:70%;
color:#343434;
font-size:20px;
margin:30px auto;
text-align:left;
padding-left:20px;
padding-bottom:50px;
}
.error404 h3 {
width:70%;
border:none;
margin:auto;
padding-bottom:20px;
border-bottom:1px solid #343434;
}
.errorBox p {
padding: 10px 15px 10px 15px;
}
/* ---------- CSS Pager ---------- */
.pagination{
padding: 2px;
}
.pagination ul{
margin: 10px 0 0 0;
padding: 0;
text-align: center;
font-size: 14px;
}
.pagination li{
list-style-type: none;
display: inline;
padding-bottom: 1px;
}
.pagination a, .pagination a:visited{
padding: 0 5px;
border: 1px solid #9aafe5;
text-decoration: none; 
color: #2e6ab1;
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
-moz-border-radius-bottomright: 5px;
-moz-border-radius-bottomleft: 5px;
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-webkit-border-bottom-left-radius: 5px;
-webkit-border-bottom-right-radius: 5px;
}
.pagination a:hover, .pagination a:active{
border: 1px solid #2b66a5;
color: #343434;
background-color: #E6EAEA;
font-weight:bold;
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
-moz-border-radius-bottomright: 5px;
-moz-border-radius-bottomleft: 5px;
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-webkit-border-bottom-left-radius: 5px;
-webkit-border-bottom-right-radius: 5px;
}
.pagination a.currentpage{
background-color: #2e6ab1;
color: #FFF !important;
border-color: #2b66a5;
font-weight: bold;
cursor: default;
}
.pagination a.disablelink, .pagination a.disablelink:hover{
background-color: white;
cursor: default;
color: #929292;
border-color: #929292;
font-weight: normal !important;
}
.pagination a.prevnext{
font-weight: bold;
}
/* ---------- formulaire d'edition ---------- */
...

 

On peut ensuite ajouter une action de minimisation avec une expression régulière et des replace() mais cela reste coûteux en PHP.

Niveau performance le script est très rapide, environ 150-200ms au maximum (YSlow) et une grande économie de requêtes HTTP.

 

Comparaison avec/sans cache willdurand.fr

Ci-dessus la comparaison est flagrante : 34 requêtes HTTP sans cache et seulement 4 avec. Pour ce qui n'est pas en cache, nous avons le statut Twitter (appel AJAX qui retourne un fichier JSON) et les deux petits widgets FeedBurner. Gain très intéressant.

 

Aperçu de l'onglet Réseau dans Firebug

Ce graphique montre l'enchainement des éléments lors du chargement de la page index. Le code PHP n'est pas optimisé, il prend près de 2 secondes et généralement 2 à 3 secondes. Pour le reste, 188ms pour charger tous les fichiers CSS (environ 15 à 20) et 384ms pour afficher tous les fichiers JavaScript (une dizaine de fichiers dont l'API Scriptaculous/Prototype). Le temps de chargement final est de 4,65 secondes, ce qui est dû en partie aux appels FeedBurner et Twitter.

Les images statiques du site sont optimisées, elles se chargent très vite. Celles dans les articles ne le sont pas et font perdre en performance.

 

Pour conclure, ces optimisations futiles mais tellement performantes me font arriver à la conclusion suivante : pourquoi se focaliser sur PHP ? Et pas regarder tout ce qui se trouve autour ? (idée reprise d'Eric Daspet mais tellement vraie). Les médias de ma page se chargent en moins de 2 secondes, j'ai pas mal de JavaScript par défaut (syntaxHighlighter se charge quoi qu'il arrive, Scriptaculous également) et c'est finalement mon code PHP qui prend le plus de temps. Normal, mon MVC a été réalisé lors de mes études et je n'avais pas connaissances de beaucoup contraintes. Aujourd'hui wMVC prend en compte ces contraintes, je passerais ce site sous wMVC et j'espère vraiment gagner en temps d'exécution. J'ai entamé un vrai travail sur wMVC pour qu'il soit le plus rapide et léger en exécution.

Commentaires

Ajouter un commentaire

avatar

Benoît  (06 June 2009 - 13:57:40)

Plutot que de passer ce genre de chose au client :



<link rel="stylesheet" type="text/css" href="http://www.willdurand.fr/cache/combo_css.php?files=css/fixIE.css|css/style.css|css/pager.css|css/form_edit.css|css/form_edit_post.css|css/mod_edito.css|css/mod_search.css|css/mod_archive.css|css/mod_popular_posts.css|css/lightbox.css|css/user.css|css/mod_stay_in_touch.css|css/twitter.css|css/syntaxHighlighter/shCore.css|css/syntaxHighlighter/shThemeDefault.css|" media="screen,projection,print" />




Tu devrait plutot lui passer qqch de ce genre :



<link rel="stylesheet" type="text/css" href="http://www.willdurand.fr/cache/generated.css?20090619120000" media="screen,projection,print" />



et ton "generated.css", tu devrais le générer toi même par ton script, uniquement quand tu changes un css.



En créant le link css pour le client, par PHP récupère la date de création du fichier, que tu passes en GET, ça te permettra de conserver le cache des navigateurs sans avoir a te préocupper des conséquences quand tu re-génères ton css.



De cette manière tu gagnes du temps processeur coté serveur puisque tu ne le créé plus à la volée, ce qui te premets au passage de servir ton client plus rapidement.



Tu gagnes également des octets sur la taille des url lors des échanges client-serveur, car je te rappel que jusqu'à IE6, les url ne doivent pas dépasser 255 caractères ^^



avatar

mattGNU  (06 June 2009 - 13:57:44)

Très bon article, reste à voir pour combien de fichiers ça devient vraiment intéressant de mettre tout ça en place.
avatar

Will  (06 June 2009 - 13:57:44)

@mattGNU : Merci. Disons que c'est pratique lorsque tu as pas mal de css (assez petits) qui sont chargés dynamiquement (selon les besoins).

 

@Benoit : Oui ok pour l'url, quoiqu'il y a moyen d'optimiser, en mettant par exemple le nom du fichier sans path ni extension. Après pour les fichiers préchargés, où est la différence entre un seul CSS uni et ton idée ? Là, je fais ça parce que mes CSS et JS sont chargés à la volée en fonction des besoins de la page.

avatar

Benoît  (06 June 2009 - 13:57:44)

Le soucis en fait de ton truc, c'est que sur chaque page tu as une liste css différente tu m'expliquais. Du coup c'est dommage, car je suppose que certaines pages partagent les mêmes css, tu te retrouves donc a fournir le même fichier css à travers plusieurs pages différentes, mais concaténé avec des css différents à chaque fois, tu perds donc l'intérêt du cache là non ?

avatar

Will  (02 July 2009 - 17:26:09)

Je vois bien ce que tu veux dire, comme on l'a dit aujourd'hui il y a toujours moyen d'optimiser quelque chose et de manière différente pour aboutir.

Cette solution n'est peut-être pas la meilleure dans le sens où, comme tu le disais, on perd en perfermance si, sur 10 fichiers CSS concaténés, 1 fichier est modifié. Effectivement, tout le cache sera regénéré. Il faudrait pré-caché et compressé les fichiers un à un, puis concaténer ces fichiers là. Le problème reste identique mais on aura gagner un peu ^^

Ce que je retiens moi c'est le ratio 34/4 ! Et le temps de rechargement de ma page =)

On pourrait en discuter des heures, le sujet est très large. En tout cas moi, mes médias sont bien optimisés, reste plus que le code PHP maintenant ;-)