'>

CSS au régime dans drupal

Allégez vos pages ! Il s'agit d'un cri du coeur. Qui n'a jamais pesté contre les temps de chargement d'une page ?
La position habituelle est de considérer que la bande passante est libérée. Erreur !

La position habituelle est de considérer que la bande passante est libérée. Erreur !
La bande passante est plus maltraitée que jamais. 2 facteurs :
  • la taille des fichiers,
  • le nombre de fichiers.
La bande passante corrige facilement le problème posé par les fichiers lourds. Mais ces mêmes fichiers ont progressé en même temps que la bande passante.
Par contre, la bande passante a un impact limité sur le problème du nombre de fichiers. Pour chaque fichier nécessaire à l'affichage de votre page, un navigateur va émettre une requête HTTP GET. Ces requêtes seront chacunes traitées individuellement par le serveur.
Pour chaque requête, le serveur préparera un environnement
d'exécution (réservation mémoire, variables d'environnement, communication inter-processus, etc.) qu'il devra ensuite nettoyer pour permettre la réception de nouvelles requêtes. Ces temps de fonctionnement sont incompressibles. Quelle que soit la taille des informations, il existe toujours une durée minimum de traitement.
Cette durée de traitement a un impact important sur le transfert de petits fichiers.
Or, les pages web sont de plus en plus peuplées de fichiers :
  • images de fond,
  • images,
  • scripts javascript,
  • feuilles de styles CSS,
  • objets Flash,
  • applets Java,
  • icône Favicon,
  • ...
Celles ou ceux qui m'auront suivi jusque là se seront sans doute posé la question : il s'agit d'une situation générale, en quoi cela concerne-t-il Drupal précisément ?
La solution que je vais présenter permet d'alléger les fichiers CSS dans un thème sous Drupal.
Prenons une page au hasard de Drupal.org : http://drupal.org/node/100748
(le sujet importe peu).
Une analyse des fichiers nécessaires à cette page donne les résultats suivants :
  • 1 fichier html : 16,17 Kio,
  • 9 feuilles CSS : 44,63
    Kio,
  • 12 images de fond : 3,14 Kio,
  • 4 images : 4,38 Kio.
Le tout nous donne 68,32 Kio pour 26 fichiers. Les feuilles de style représentent un tiers des fichiers téléchargés et 65% du volume total !
 
L'idée pour faire perdre du poids aux feuilles CSS est la suivante :
  • regrouper tous les fichiers en un seul,
  • supprimer tous les espaces, les retours chariots et les commentaires.
Réaliser ces opérations manuellement n'est ni pratique, ni maintenable. Mais elles peuvent être automatisées dans le cadre de votre thème.
Avec l'aide d'un script PHP, il est possible de réaliser ces opérations à la "volée".
Le script compress_css.php se trouve sur la page suivante et doit être copié dans le répertoire de votre thème.
Pour l'utiliser dans votre thème, éditer le fichier page.tpl.php et insérez le code suivant :
<?phpinclude 'compress_css.php';
print
compress_tout_les_styles($head.$styles);?>
en lieu et place des print $head; et print $styles;
Le script PHP va lire tous les fichiers CSS inclus dans l'entête HTML (balise head), les réunir en un seul, supprimer les élements inutiles et insérer le code pour la nouvelle feuille de style.
Pour exemple, l'utilisation de ce script m'a permis de passer de 50 Kio de CSS à 35 Kio (environ 30% de gain). C'est très appréciable sachant que certains standards d'accessibilité (voir Opquast) recommendent d'avoir une page d'accueil pesant moins de 100 Kio tout compris.
 
Le script ci-dessous doit être copié dans le répertoire de votre thème pour pouvoir être utilisé.
Le répertoire doit être en écriture pour Apache (généralement 0777) car la version compressé des CSS est écrit dans un fichier *.compress.css (afin de ne pas recompresser les fichiers à chaque fois)
Le script prend en compte le "hack IE", celui qui fonctionne avec les commentaires, de sorte que ces commentaires ne soient pas effacés.
<?phpfunction compress_css($base,$dir) {
 
// Lecture du fichier source
 
$css=file_get_contents($dir.'/'.$base);   // Inclusion des CSS importés
 
$pattern='/\@import url((.<em>.css));/e';
 
$replace='compress_css("\1","'.$dir.'")';
  $css=preg_replace(
   
$pattern,
   
$replace,
   
$css
 
);
  // Transforme les retours chariots Windows en Unix
 
$css=str_replace(chr(13),chr(10),$css);
  // Supprime les commentaires
 
$debut_commentaire=strpos($css,'/</em>');
 
$precedent        ='normal'; // Gestion du hack IE
 
while($debut_commentaire!==FALSE) {
   
$fin_commentaire=strpos($css,'<em>/',$debut_commentaire+2);
    if(
$fin_commentaire===FALSE) break;
    // Gère le hack IE
   
if(substr($css,$fin_commentaire-1,1)=='\') {
      // Le commentaire est un Hack pour IE, on garde le commentaire en le vidant
      $css=substr($css,0,$debut_commentaire).
           '
/</em>*/'.
           substr($css,$fin_commentaire+2);

      // Le prochain commentaire doit être conservé pour terminer le hack
      $precedent='hack';
    } else {
      // Si le précédent commentaire était un hack,
      // le commentaire actuel doit être gardé pour terminer le hack
      if($precedent=='
hack') {
        $css=substr($css,0,$debut_commentaire).
             '
/**/'.
             substr($css,$fin_commentaire+2);
      } else {
        // Commentaire normal, on le supprime complètement
        $css=substr($css,0,$debut_commentaire).
             substr($css,$fin_commentaire+2);
      }
     
      $precedent='
normal';
    }

    $debut_commentaire=strpos($css,'/<em>',$debut_commentaire+1);
  }

  // Les identifiants se suffisent à eux-mêmes, pas besoin de préciser l'élément
  $pattern
='/[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_]+#/';
 
$replace='#';
  $css=preg_replace(
   
$pattern,
   
$replace,
   
$css
 
);
  // Transforme les retours chariots en espace
 
$pattern='/'.chr(10).'+/';
 
$replace=' ';
  $css=preg_replace(
   
$pattern,
   
$replace,
   
$css
 
);
  // Transforme les espaces multiples en espaces simples
 
$pattern='/[ '.chr(9).']+/';
 
$replace=' ';
  $css=preg_replace(
   
$pattern,
   
$replace,
   
$css
 
);
  // Supprime les espaces inutiles
 
$pattern='/[ '.chr(9).']</em>([:;{},])[ '.chr(9).']<em>/';
 
$replace='\1';
  $css=preg_replace(
   
$pattern,
   
$replace,
   
$css
 
);
  // Les points-virgules sont inutiles en fin de block (juste avant l'accolade fermante)
 
$css=str_replace(';}','}',$css);
  // Retourne le CSS compressé
 
return $css;
}

function recupere_liste_fichiers_css($styles) {
 
$liste=array();
  // Récupère les @import
 
$pattern='/\@import "([^;]</em>.css)";/';
 
$matches=array();
  preg_match_all($pattern,$styles,$matches,PREG_PATTERN_ORDER);
  $liste=array_merge($liste,$matches[1]);
  // Récupère les link rel="stylesheet"
 
$pattern='/<link rel=["\']stylesheet["\'][^>]<em>href=<a href="/%5B%5E%3E%22%5C%26#039;]+" rel="nofollow">"\'</a>["\'][^>]</em>\/>/';
 
$matches=array();
  preg_match_all($pattern,$styles,$matches,PREG_PATTERN_ORDER);
  foreach($matches[1] as $url) {
    if(
substr($url,0,7)=='http://') {
     
$mon_adresse='http://'.$_SERVER['HTTP_HOST'].'/';
      if(
substr($url,0,strlen($mon_adresse))==$mon_adresse) {
       
$url=substr($url,strlen($mon_adresse)-1);
      }
    }

    $liste[]=$url;
  }

  return $liste;
}

function nettoie_styles($styles) {
 
// Retire les style en @import
 
$pattern='/<style[^>]*>\@import[^<]*<\/style>/';
 
$replace='';
  $styles=preg_replace($pattern,$replace,$styles);
  // Retire les style en link rel
 
$pattern='/<link rel=["\']stylesheet["\'][^>]*\/>/';
 
$replace='';
  $styles=preg_replace($pattern,$replace,$styles);
  return $styles;
}

function compress_tout_les_styles($styles) {
 
$liste=recupere_liste_fichiers_css($styles);
 
 
$empreinte=hash('md5',implode('|',$liste));
 
 
$fichier_all    =dirname(__FILE__).'/'.$empreinte.'.compress.css';
 
$fichier_all_web=substr($fichier_all,strlen($_SERVER['DOCUMENT_ROOT']);
  if(!is_readable($fichier_all)) {
   
$total='';
    foreach(
$liste as $fichier) {
     
$chemin=$_SERVER['DOCUMENT_ROOT'].$fichier;
     
$total.=compress_css(basename($chemin),dirname($chemin));
    }

    file_put_contents($fichier_all,$total);
  }

  $styles=nettoie_styles($styles);
 
$styles.='<style type="text/css" media="all">@import "'.$fichier_all_web.'";</style>';
 
  return
$styles;
}
?>