J'ai longuement travaillé sur l'optimisation des temps d'affichage de ce blog pendant les vacances de Noël. Je pense avoir atteint les performances maximales, je n'ai plus vraiment de pistes pour aller plus loin. La page d'accueil du blog s'affiche désormais en 300 ms.
Au niveau Google Analytics, le temps d'affichage moyen des pages par tous les visiteurs français qui visitent ce site est de 3,19 secondes. J'ai tout de même gagné 5 secondes de temps de chargement par rapport à mes dernières statistiques.

Limitation du nombre de ressources à charger

Contrairement à la précédente version du blog, les pages HTML ne contiennent plus qu'un petit nombre de ressources :

  • Tous les arrondis des "div" et les dégradés sont désormais gérés en CSS3 et plus par des images.
  • Toutes les icônes proviennent d'une seule et même image.

La réduction du nombre de ressources à charger favorise les temps d'affichage de la page, d'autant plus que les navigateurs sont souvent configurés pour réaliser 8 requêtes simultanées vers un même serveur.
Lorsque le navigateur a 16 ressources à charger, celui-ci va d'abord télécharger les 8 premières puis démarrer le téléchargement des 8 autres ressources dès que les premières ressources ont été téléchargées.

Chargement des ressources en asynchrone

J'utilise la librairie Modernizr pour gérer la compatibilité des fonctionnalités HTML5 embarquées avec les vieux navigateurs et pour charger de manière asynchrone les ressources qui n'ont pas besoin d'être chargées au rendu de la page.
Cela permet à la page de s'afficher dans un temps record sans être bloquée par le chargement de ressources externes.

Tous les traitements JQuery sont exécutés une fois la page chargée.


Modernizr.load([
  {
    load: '//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js',
    complete: function () {
      if ( !window.jQuery ) {
            Modernizr.load('https://static.geeek.org/themes/geeek/js/vendor/jquery.min.js');
      }
    }
  },
  'https://static.geeek.org/themes/geeek/js/vendor/bootstrap.min.js',
  'https://static.geeek.org/themes/geeek/js/main.js'
]);

Mise en cache des ressources statiques au niveau du navigateur

Toutes les ressources statiques sont mises en cache au niveau du navigateur. J'ai modifié la configuration de mon serveur Web pour qu'il force le cache et la compression de toutes les ressources statiques délivrées.
Les ressources statiques sont délivrées par un domaine sans cookie "static.geeek.org" afin de garantir l'utilisation des images dans le cache par le navigateur et les potentiels proxy.

Enfin, le serveur supporte les Etag et le Header HTTP "If-none-match" afin d'éviter de faire télécharger au navigateur une image qu'il aurait déjà téléchargé auparavant.


$HTTP["host"] == "static.geeek.org" {

  compress.cache-dir = "/home/servers/static.geeek.org/cache/"
  compress.filetype = ("text/css","application/javascript")

  expire.url = (
    "/public/" => "access 2 months",
    "/themes/" => "access 2 months",
    "/plugins/" => "access 2 months",
  )

  static-file.etags="enable"
  setenv.add-response-header = ( "Cache-Control" => "public" )
}

Mise en cache des pages HTML côté serveur

Toutes les pages HTML sont stockées dans un cache Memcached au niveau du serveur. Lorsque votre navigateur demande une page Web au blog, dans 99% des cas, c'est le cache qui vous délivre la page. La plateforme de blog est réellement sollicité que très rarement. Cela permet de gagner 60 à 80% de CPU au niveau du serveur et de réduire de 100ms à 200ms le temps de fourniture d'une page du blog.

workflow-http.png

Réduction du code PHP exécuté

Afin d'optimiser les temps d'exécution de la plateforme de blog, le fichier index.php de ma plateforme de blog Dotclear a été remplacé par petit script PHP optimisé qui a comme seul objectif de vérifier si oui ou non il est nécessaire de solliciter la plateforme de blog. Ce script n'ayant aucune dépendance, il s'exécute très rapidement et consomme que très peu de CPU.

Le temps de traitement au niveau de mon serveur pour délivrer une page du blog est désormais de 40ms, si celle-ci se trouve dans le cache.

La prochaine étape est de migrer ce script PHP en LUA et de l'intégrer directement au niveau du serveur Web pour éviter de solliciter le pool de CGI PHP.

architecture-blog-performant.jpg

Voici à titre d'exemple, le script utilisé :



/**
 * Check if the page is elligible for the cache
 */
function elligibleForCache(){

 //global $logger;
 if (count($_POST) == 0 && (count($_GET) == 0 || isset($_GET['utm_source']))){
      return true;
 }
 return false;
}

require_once 'inc/libs/clearbricks/common/lib.http.php';
$uri = http::getSelfURI();

// Start a new Memcache connection
$mc = new Memcache();
if (!$mc->pconnect("localhost", "11211")) {
        throw new Exception('Unable to connect to memcached.');
}

// Check if the page is elligible for the cache
if (elligibleForCache()) {

  $uri = urldecode($uri);
  $uri = explode('?',$uri);
  $uri = explode('#',$uri[0]);
  $uri = md5($uri[0]);

  $page = @unserialize($mc->get($uri));

  if($page != null){
      deliverPage($page,true);
      exit;
  }
}


La méthode deliverPage() s'occupe de délivrer le contenu récupéré du cache :


/**
 * Deliver a page
 */
function deliverPage($page, $cache=false){

 // Check the Etag
 if ((isset($_SERVER['HTTP_IF_NONE_MATCH']) &&  trim($_SERVER['HTTP_IF_NONE_MATCH']) != $page['etag'])){
      // Nothing
 }

 // Check the Etag & the If modified since ...
 else if ((isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && trim($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $page['upd
dt']) || (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&  trim($_SERVER['HTTP_IF_NONE_MATCH']) == $page['etag'])){
    header("HTTP/1.1 304 Not Modified");
    exit;
 }


 // If client supports GZIP
 if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strstr($_SERVER['HTTP_ACCEPT_ENCODING'], "gzip")){
       header("Content-Encoding: gzip");
       $content = $page['gzcontent'];
       $length = $page['gzcontent_length'];
 } else {
       $content = $page['content'];
       $length = $page['content_length'];
 }

 header('Content-Length: '.$length);
 header('Content-Type: '.$page['content_type'].'; charset=UTF-8');
 header('Last-Modified: '.$page['upddt']);
 header('ETag: '.$page['etag']);

 echo($content);
}


L'objet $page est stocké manuellement dans le cache via la modification de la méthode "serveDocument" de Dotclear.


''Crédits photo du header: Express Monorail''