20071011

Cache and GZIP your javascripts and CSSs files to speed up your site



I've been trying to mix different sources to optimize the download speed of the javascript files and the CSS files of any web application, in particular I've worked for the IPSOJobs new urgent job bank web page, but it can be applicable in a more general way.

Your Web application can use this files if:


  • You can use .htaccess in your hosting

  • You have CSS only and JS only folders to apply to the whole folder

  • You can use PHP in your hosting



If all the above conditions are met, you can simply add three files to your CSS and/or JS folder and you are done.

Please, take with caution and do some testing, don't use directly in a production environment.


Of course, I'll not give any waranty of succes and you have to check the results using the Firebug Extension of Firefox, the "Live HTTP Headers" extension and the YSlow plugin of the Firebug Extension.

First file .htaccess:
Modify your .htaccess file to include the following:


AddHandler application/x-httpd-php .css .js
php_value auto_prepend_file gzip-start.php
php_value auto_append_file gzip-end.php


This code, tells Apache to automatically run the gzip-start.php before the requested javascript or css file and to run the file gzip-end.php after the conclusion of the requested file.

It's a powerful way to create common headers and footers to pages or files, but be aware that it comes at a cost, it's more fast to serve the static file alone than to run an php script before and one after the file, of course, you have to do some testing and see if it's worthy to do so.

Second file, the magical gzip-start.php:



function get_http_mdate()
{
return gmdate('D, d M Y H:i:s', filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF'])).' GMT';
}

function check_modified_header()
{// This function is based on code from http://ontosys.com/php/cache.html
$headers=apache_request_headers();
$if_modified_since=preg_replace('/;.*$/', '', $headers['If-Modified-Since']);
if(!$if_modified_since)
{
return;
}

$gmtime=get_http_mdate();

if ($if_modified_since == $gmtime)
{
header("HTTP/1.1 304 Not Modified");
exit;
}
}

check_modified_header();

// Open a gzipped buffer
ob_start ("ob_gzhandler");

if (strpos($_SERVER['PHP_SELF'], '.js')>0)
{// Javascript file
header("Content-type: text/javascript");
}
elseif (strpos($_SERVER['PHP_SELF'], '.css')>0)
{// CSS file
header("Content-type: text/css");
}

$offset = 60000000; // Far far away in time

// Expires
header("Expires: ".gmdate("D, d M Y H:i:s", time() + $offset) . " GMT");

// Cache-Control
header("Cache-Control: must-revalidate, max-age=".(time()+$offset));

header("Last-Modified: ".get_http_mdate());

// generate unique ID, using the modification date and the absolute path to the file
$hash = md5(filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']).$_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']);
header('Etag: "'.$hash.'"');

?>


The concept is clear, this file is interpreted by the PHP runtime rigth before the output of the requested file.

Let's see the different parts of the file:


check_modified_header();


This function looks for the modification date of the requested file (filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF'])) and returns a HTTP 304 (Not Modified) if the file is fresh, to do that, first looks for a request header If-Modified-Since and then compare the dates.


// Open a gzipped buffer
ob_start ("ob_gzhandler");

if (strpos($_SERVER['PHP_SELF'], '.js')>0)
{// Javascript file
header("Content-type: text/javascript");
}
elseif (strpos($_SERVER['PHP_SELF'], '.css')>0)
{// CSS file
header("Content-type: text/css");
}


These lines opens a PHP buffer with the trick that it's using GZip compression, this is a very good feature of PHP that enables you to output gzipped content without any modification. Afterwards we put the Content-Type of the resulting file, depending of the extension of the file we put text/css or text/javascript.


$offset = 60000000; // Far far away in time

// Expires
header("Expires: ".gmdate("D, d M Y H:i:s", time() + $offset) . " GMT");

// Cache-Control
header("Cache-Control: must-revalidate, max-age=".(time()+$offset));


These lines put the Expires and Cache-Control headers, both far away in time, we create the variable offset with a constant 60 million seconds (about 2 years) and use that to generate a Expires header and a Cache-Control header with the value of two years in the future.

Be warned, your CSS and your JS files will be cached in a lot of proxies and client browsers, you have to use a "rename file policy" in your HTML to prevent the users from using old versions of the files. We've got little javascripts and css files to change, but I recommend to split these kind of files in different folders, for instance /js/usually-modified/omatech.js and apply only the caching technique to the folder usually-modified, then rename each time the .js file with a timestamp, say omatech_20071010.js, and change your html code to reflect the new version.



header("Last-Modified: ".get_http_mdate());

$hash = md5(filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']).$_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']);
header('Etag: "'.$hash.'"');


The last lines, creates a Last-Modified header, used to indicate the client browser the freshness of the file for future requests, this have interactions with the Cache-Control/max-age and the Expires headers (out of the reach of this post).

Finally we generate an unique Etag header, based on the update time of the requested file and it's full path.


Third and last file, the not less magical gzip-end.php:


header('Content-Length: ' . ob_get_length());
ob_end_flush();
?>


This file simply creates dinamically a Content-Length header with the size of the gzipped buffer we have been using for this request, and then flushes it's contents.

Simple eh ?


I hope you enjoyed and finded useful, feel free to change and play with the code.


I would like to thank the sources where I took inspiration and code:

The tutorial of CSS compression,
Facilitate User Experience with CSS Compression, from Max Kiesler

Good introduction to the problem of speeding up the webpages through caching and zipping javascripts
Serving Javascript Fast, by Cal Henderson

And always a good tutorial even for novices (in Spanish)
¿Por qué y cómo crear un espacio web "cache friendly"? from jcea

3 comentarios:

Seize dijo...

Please, take with caution and do some testing, don't use directly in a production environment.

Spanish translation:

Las pruebas en casa y con gaseosa!

Rob dijo...

Just a bit improvement... didn't test yet, but... isn't it better to call the files this way?
.gzip-start.php
.gzip-end.php
So Apache won't load them if they are called up directly in the URL, like http://www.domain.com/js/.gzip-start.php
(what will happen without the dot?)

Urko dijo...

I think that it should start php tag in gzip-end.php

Great tutorial! Thanks

Urko