Mais pourquoi ce site est-il aussi rapide ?

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('http://static.geeek.org/themes/geeek/js/vendor/jquery.min.js');
}
}
},
'http://static.geeek.org/themes/geeek/js/vendor/bootstrap.min.js',
'http://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.
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.
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



Bon travail !
Belle opti, notamment sur le script php qui squiz l'accès à la plateforme!
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... ?
Génial comme article!
Moi aussi je note une nette amélioration.
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
jolie travail, ton site sobre, rapide, plaisant. Il est tooo geeek ;o)
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 !
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.
C'est vrai que c'est très rapide, même avec 512k, merci !
@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
Très beau travail Ludovic !
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/
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(){
}
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.