• Ce blog — désormais archivé — est en lecture seule.

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

Aprè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="/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="/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,h2,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;
}
h2, 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.

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.

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.

  • Print
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Twitter
  • Google Bookmarks
  • FriendFeed
  • LinkedIn
  • MySpace
  • Netvibes
  • PDF
  • Ping.fm
  • RSS
  • Technorati
  • viadeo FR
  • Wikio
  • Yahoo! Buzz

Related Posts

Cet article a été publié dans Ancien blog avec les mots-clefs : , , , , , , . Bookmarker le permalien. Les commentaires et les trackbacks sont fermés.