speed-performance.jpg

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

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

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

1. Le , 15:46 par jeremy
c19f30ec4077e6f69df185c701a8c6df

Bon travail !

2. Le , 16:11 par IBremen
b5e95abfc9622d30a4ba63d9ea03584a

Belle opti, notamment sur le script php qui squiz l'accès à la plateforme!

3. Le , 16:53 par Loni
d74f0d2c5efdf493302aa1da2c1d7e9d

J'avoue que la rapidité rox du poney.

Par contre pour gagner un peu plus niveau home, ça serait peut être mieux de ne mettre qu'un extrait de chaque billet, histoire de charger moins de choses... ?

4. Le , 17:19 par TheArsenik
8589d163db948b37fd4822de1d3d0096

Génial comme article!
Moi aussi je note une nette amélioration.

5. Le , 20:03 par Mathcoll
d8122c6b61dcc7c68ecaf56692f6b696

Hello,

Est ce que tu pourrais nous en dire plus sur le "petit script" qui vérifie si oui ou non il est nécessaire de solliciter la plateforme de blog ?
quel language ? comment il fonctionne ? quelle licence, c'est toi qui l'a fait ? il s'appelle comment ? ... ... ..

merci

6. Le , 23:05 par mescanefeux
739ace47a5ca3fb0a0b65d0eca811ad1

jolie travail, ton site sobre, rapide, plaisant. Il est tooo geeek ;o)

7. Le , 00:01 par Ludovic
9ab09dd3e305f924f8930e20e1a35843

Merci pour vos commentaires !

@Mathcoll : J'ai collé le code que j'utilise en bas de l'article. Si vous avez des propositions d'amélioration, n'hésitez pas !

8. Le , 14:44 par felix_m
68e19e7a9473b8fdce8a70d97ee2e2fd

Bonne idée d'utiliser memcache!

Pour les moins programmeurs, Cloudflare offre un super service gratuit de cache.

Sinon une cache HTML5 (cache.manifest) pourrait améliorer davantage le chargement côté client.

9. Le , 16:46 par kaloskagatos
ab52b8ea0f2632e8bc8aeca1ec19f3ef

C'est vrai que c'est très rapide, même avec 512k, merci !

10. Le , 20:02 par Ludovic
9ab09dd3e305f924f8930e20e1a35843

@felix_m : J'y ai pensé .. et même fait ... Le seul problème vient du lazy caching sur les pages HTML.
J'ai finalement préféré laissé gérer le cache des ressources statiques par un simple header HTTP expire ;-)

11. Le , 23:12 par Vincent
0030fd0a82e7f41b8d35d21dc4de1e3a

Très beau travail Ludovic !

12. Le , 15:02 par defkrie
cc7e6dd8853ad620c231a59fa4403b78

C'est une jolie optimisation et du beau boulot avec une bonne utilisation de Bootstrap. Il ne te reste plus qu'à mon avis à réduire le nombre de requête en regroupant les fichiers css, js en un seul fichier, supprimant les caractères inutiles, les compressant, zlib, cache header et celà sera parfait ;)
Tu trouveras un exemple sur http://code.google.com/p/minify/

13. Le , 23:32 par Gautier
baecaa353a41429a012e54674875b4dd

J'ai pas tout lu, juste survolé depuis mes RSS et ça, ça pique un peu les yeux :

function elligibleForCache(){

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

}

Qui peut s'écrire :

function elligibleForCache(){

 return (count($_POST) == 0 && (count($_GET) == 0 || isset($_GET['utm_source'])));

}

C'est peut être pour le logger ou un je ne sais quoi, mais on ne peut parler d'optimisation, alors que le principe de base c'est d'avoir qu'un seul return par fonction.

Cordialement.

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.