Gestione CSS avanzata: Parte 3

cssProseguendo con il nostro tutorial è giunto il momento di ottimizzare un po’ le prestazioni aggiungendo la gestione della cache.

Come accennato in precedenza, dobbiamo tenere conto di 2 tipi di cache:

  1. Cache lato browser per ridurre il numero delle richieste
  2. Cache lato server del css precompilato

Cache lato browser

Forniremo 3 header al browser per fargli fare cache del nostro css finale:

  1. Last-Modified: utilizzando la data di ultima modifica del file modificato più di recente tra quelli richiesti
  2. Etag: utilizzando la data di ultima modifica e la dimensione di tutti i file richiesti
  3. Expires

Potrebbe sembrare superfluo l’uso degli header Etag/Last-Modified insieme a Expires, ma è comunque conveniente utilizzare entrambi combinatamente.
Una risorsa che viene immagazzinata in cache tramite header Expires non genera nessuna richiesta HTTP da parte del browser, a differenza della risorsa Etag che genera sempre una richiesta e se il contenuto non è stato modificato, non scarica alcun dato oltre agli header della risposta.

Flow Chart Cache Browser

La prima cosa da fare quindi è leggere data di ultima modifica e dimensione di ogni file richiesto e genero un etag aggregando i dati. Per l’header Last-Modified invece utilizzo solo la data di modifica più recente tra quella dei file richiesti.

//[...]
$files = explode(' ',$_GET['f']);

$etagdata = array();
$lastmtime = 0;
foreach ($files as $file) {
    $file = trim($file);
    $template = "css/$file.ccss";
    if (!$s->templateExists($template))
        $template = "css/$file.css";
    $template_file = $s->template_dir.DIRECTORY_SEPARATOR.$template;
    if ($s->templateExists($template)) {
        $mtime = filemtime($template_file);
        $etagdata[] = $mtime;
        $etagdata[] = filesize($template_file);
        if ($mtime>$lastmtime)
            $lastmtime = $mtime;
    }
}

// Calcolo un etag e lo comunico al client
$modtime = gmdate('r', $lastmtime);
$etagdata = implode(':', $etagdata);
$etagdata = md5($etagdata.':'.$modtime);
$etag = "\\"$etagdata\\"";
header("Last-Modified: $modtime");
header("Etag: $etag");
// Impostiamo anche una data di scadenza per ridurre le richieste, ma nel futuro prossimo (4H) dato che abbiamo anche gli ETag
header('Expires: ' . gmdate('D, d M Y H:i:s', time()+14400) . ' GMT');

// Verifico l'etag richiesto dal client ed eventualmente interrompo lo script
if (
    (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $_SERVER['HTTP_IF_MODIFIED_SINCE']==$modtime)
    ||
    (!empty($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH']==$etag)
) {
    header('HTTP/1.1 304 Not Modified');
    header('Content-Length: 0');
    exit;
}

Come tocco finale aggiungiamo anche la compressione gzip dei contenuti:

if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false)
    ob_start('ob_gzhandler');

Cache lato server

Il nostro obiettivo è di evitare di processare i file sorgenti ogni volta, ma farlo solo quando questi vengono modificati.
Per farlo salviamo il file finale in un file di cache e impostiamo la sua data di ultima modifica all’ultima data di modifica dei file sorgenti.
Alle richieste successive verifichiamo che questo file esista e che la sua data di modifica non sia inferiore a quella dei file sorgenti e in tal caso serviamo il file precompilato.

//cache dir
if (!file_exists("$basedir/cache/css")) {
    mkdir("$basedir/cache/css");
    chmod("$basedir/cache/css", 0777);
}

$cachefile = "$basedir/cache/css/$etagdata.css";
if (!file_exists($cachefile) || filemtime($cachefile)<$lastmtime) {

    $csscontent = "";
    foreach ($files as $file) {
        $template = "css/$file.ccss";
        // Se il file non esiste tra i CCSS (CleanCSS), allora lo cerco come CSS normale
        if (!$s->templateExists($template)) {
            $template = "css/$file.css";
            $csscontent .= "/* $file.css */\\n";
            $csscontent .= trim($s->fetch($template))."\\n";
        } else {
            $csscontent .= "/* $file.ccss */\\n";
            $csscontent .= CleanCSS::convertString($s->fetch($template))."\\n";
        }
    }

    file_put_contents($cachefile, $csscontent);
    chmod($cachefile, 0666);
    touch($cachefile, $lastmtime);
    echo $csscontent;
} else {
    echo file_get_contents($cachefile);
}

I file precompilati andranno a finire nella cartella cache/css e ricordiamoci quindi di cancellarli se per qualsiasi motivo dobbiamo forzarne la rigenerazione.

Il codice sorgente completo è disponibile per il download. Come per la puntata precedente mi raccomando di assicurarvi di dare i permessi di scrittura alla cartella cache.

Col prossimo articolo si concluderà la serie sulla gestione avanzata dei css aggiungendo la generazione dinamica delle varianti delle regole e la minificazione del codice.