20 January 2010 ~ 19 Comments

CSS Minification on the Fly

Introduction

So, I  recently took the habit to fire Google’s Page Speed to learn about my websites performances and looking for ways to optimize them for my particular hosting solution and for the benefits of my readership (you).

One of the recommendation was to minify CSS. We (web workers) have all encountered JavaScript minification. This became a common task to do at the pre launch phase.

Assets minification is great, it’s saves bandwidth and reduces latency, however there is a downside, you need to keep copies of the original files if you want to keep any formating.

That’s where I come with a simple solution to minify on the fly any css embedded in your site. This will involve a little knowledge of PHP and Apache rewrite rules.

The PHP minifier

Ok, I don’t like to reinvent the wheel when I know such a wheel exists. So I borrowed and slightly edited the php css minifer published on lateralcode.com.

[sourcecode lang="php"]
header( "Content-type: text/css" );

$file = isset( $_GET[ 'css' ] ) ? $_GET[ 'css' ] : '';

$content = file_get_contents( $_SERVER['DOCUMENT_ROOT'] . $file );
echo minify( $content );

function minify( $css ) {
	$css = preg_replace( '#\s+#', ' ', $css );
	$css = preg_replace( '#/\*.*?\*/#s', '', $css );
	$css = str_replace( '; ', ';', $css );
	$css = str_replace( ': ', ':', $css );
	$css = str_replace( ' {', '{', $css );
	$css = str_replace( '{ ', '{', $css );
	$css = str_replace( ', ', ',', $css );
	$css = str_replace( '} ', '}', $css );
	$css = str_replace( ';}', '}', $css );

	return trim( $css );
}

[/sourcecode]

The Rewrite Rules

I saved my minifier PHP script into a directory named /m/ on my site’s root. All I want know is to minify all my css files without having to edit all the link tags and @import in my code. So I’ve written this rewrite rule to pass any request to a file ending in .css to my PHP minifier; It’s not a redirect it’s just a rewriting so I can keep my paths unmodified.

[sourcecode lang="bash"]
	# Avoids infinite loops
	RewriteCond %{REQUEST_URI}	!^/m/$

	# Pass the complete path to any css file to the php minifier
	RewriteRule ^/(.*\.css)$	/m/index.php?css=$1 [NC]
[/sourcecode]

WordPress Tests

Here’s the results for a request against a typical WordPress installation with a couple plugins installed :

CSS minified

CSS not minified

Here is the comparison chart of the gains on a typical request:

CSS Minifier Benefits Comparison Chart

Latency tests are not extremely accurate since it may varies depending on network connection. I, however, constated significants performances boost and on very few hosts, an unexplained drop in latency performance. My guess is that the latency was introduced by the computations in the PHP minifier script on very busy servers.

Enhancements

This “hack” is not perfect as is. You might want to generate a cache of the minified CSS files to save a significant amount of CPU cycles. Here is the modified PHP CSS minifier vastly inspired by Quick and Dirty PHP Caching article (snipe.net).

My Apache is configured to send GZIPed content whenever possible so, I dropped the part about compression and merged the different CSS minification functions to get the best out of them.

[sourcecode lang="php"]

header("Content-type: text/css");
header("Cache-Control: no-cache, must-revalidate");

$file = isset( $_GET[ 'css' ] ) ? $_GET[ 'css' ] : '';

$cachefile = 'cache/'.basename($file);
$cachetime = 24 *  60 * 60;

if (file_exists($cachefile)
&& (time() - $cachetime < filemtime($cachefile))) {
        header("Expires: " .
			gmdate("D, d M Y H:i:s", filemtime($cachefile) + $cachetime) .
			" GMT", true);
        include($cachefile);
        exit;
}

ob_start(); // start the output buffer

$fp = fopen($cachefile, 'w'); // open the cache file for writing

$content = file_get_contents( $_SERVER['DOCUMENT_ROOT'] . $file );
echo minify($content);
echo "/* Cached ".date('jS F Y H:i', filemtime($cachefile))." */";
// save the contents of output buffer to the file
fwrite($fp, ob_get_contents());
fclose($fp);

ob_end_flush(); // Send the output to the browser

function minify($data) {
    $data = preg_replace( '#/\*.*?\*/#s', '', $data );
    // remove single line comments, like this, from // to \\n
    $data = preg_replace('/(\/\/.*\n)/', '', $data);
    // remove new lines \\n, tabs and \\r
    $data = preg_replace('/(\t|\r|\n)/', '', $data);
    // replace multi spaces with singles
    $data = preg_replace('/(\s+)/', ' ', $data);
    //Remove empty rules
    $data = preg_replace('/[^}{]+{\s?}/', '', $data);
    // Remove whitespace around selectors and braces
    $data = preg_replace('/\s*{\s*/', '{', $data);
    // Remove whitespace at end of rule
    $data = preg_replace('/\s*}\s*/', '}', $data);
    // Just for clarity, make every rules 1 line tall
    $data = preg_replace('/}/', "}\n", $data);
    $data = str_replace( ';}', '}', $data );
    $data = str_replace( ', ', ',', $data );
    $data = str_replace( '; ', ';', $data );
    $data = str_replace( ': ', ':', $data );
    $data = preg_replace( '#\s+#', ' ', $data );

    return $data;
}
[/sourcecode]

More Enhancements

Well, we are now saving all minified css files into our cache so we can read from these cached files for all the next requests for 24 hours thus completly anihilating the latency problem on busy servers. That done I restored the Browser Caching leverage by adding an Expires header related to each files generation time + the cache lifetime.

But Wait, There is more! Cache Control

Addionaly, as web developers we will need something to deactivate this caching behavior. Deactivating it completly is just as easy as removing the Apache rewrite rules. But what we might find very handy is to regenerate the cache if the master CSS file has been modified.

The PHP CSS Minifier script becomes then:

[sourcecode lang="php"]
$time_start = microtime_float();

header("Content-type: text/css");
header("X-Powered-by: CSS Minifier v0.1", true);
header("Cache-Control: no-cache, must-revalidate");

$file = isset( $_GET[ 'css' ] ) ? $_GET[ 'css' ] : '';

$cachefile = 'cache/'.basename($file);
$cachetime = 24 *  60 * 60;

$age = filemtime($_SERVER['DOCUMENT_ROOT'] . $file);

header("X-Original-Generated-At: " . nicetime($age));

if (file_exists($cachefile)
&& (time() - $cachetime < filemtime($cachefile))) {
        header("Expires: " .
			gmdate("D, d M Y H:i:s", filemtime($cachefile) + $cachetime) .
			" GMT", true);
 $time_end = microtime_float();
 $time = $time_end - $time_start;
 header("Cache-Status: cached");
 header("X-Runtime: ". $time . "sec");

 include($cachefile);
 exit;
}

header("Cache-Status: regenerated");
ob_start(); // start the output buffer
$fp = fopen($cachefile, 'w'); // open the cache file for writing

$content = file_get_contents( $_SERVER['DOCUMENT_ROOT'] . $file );
echo minify($content);
echo "/* Cached ".date('jS F Y H:i', filemtime($cachefile))." */";
fwrite($fp, ob_get_contents()); // save the contents of output buffer to the file
fclose($fp); // close the file

$time_end = microtime_float();
$time = $time_end - $time_start;
header( "X-Runtime: ". $time . "sec" );

ob_end_flush(); // Send the output to the browser

function minify($data) {
 $data = preg_replace( '#/\*.*?\*/#s', '', $data );
 // remove single line comments, like this, from // to \\n
 $data = preg_replace('/(\/\/.*\n)/', '', $data);
 // remove new lines \\n, tabs and \\r
 $data = preg_replace('/(\t|\r|\n)/', '', $data);
 // replace multi spaces with singles
 $data = preg_replace('/(\s+)/', ' ', $data);
 //Remove empty rules
 $data = preg_replace('/[^}{]+{\s?}/', '', $data);
 // Remove whitespace around selectors and braces
 $data = preg_replace('/\s*{\s*/', '{', $data);
 // Remove whitespace at end of rule
 $data = preg_replace('/\s*}\s*/', '}', $data);
 // Just for clarity, make every rules 1 line tall
 $data = preg_replace('/}/', "}\n", $data);
 $data = str_replace( ';}', '}', $data );
 $data = str_replace( ', ', ',', $data );
 $data = str_replace( '; ', ';', $data );
 $data = str_replace( ': ', ':', $data );
 $data = preg_replace( '#\s+#', ' ', $data );

 return $data;
}

function microtime_float() {
 list($usec, $sec) = explode(" ", microtime());
 return ((float)$usec + (float)$sec);
}

function nicetime($date)
{
 if(empty($date)) {
 return "No date provided";
 }

 $periods         = array(
							"second", "minute", "hour",
							"day", "week", "month", "year", "decade"
					);
 $lengths         = array(
							"60","60","24","7",
							"4.35","12","10"
					);

 $now             = time();
 $unix_date         = $date;

 // check validity of date
 if(empty($unix_date)) {
 return "Bad date";
 }

 // is it future date or past date
 if($now > $unix_date) {
 $difference     = $now - $unix_date;
 $tense         = "ago";

 } else {
 $difference     = $unix_date - $now;
 $tense         = "from now";
 }

 for($j = 0; $difference >= $lengths[$j]
		&& $j < count($lengths)-1; $j++) {
 	$difference /= $lengths[$j];
 }

 $difference = round($difference);

 if($difference != 1) {
 $periods[$j].= "s";
 }

 return "$difference $periods[$j] {$tense}";
}

[/sourcecode]

Note the additional HTTP headers to keep track of cache statuses. Here's how to test the whole functionalities:

1 . No cache available

CSS Minifier No Cache

2 . File is already cache:

CSS Minifier Cached File

3 . Master CSS file has been modified:

CSS Master Modified

As you can see, the different HTTP headers : Cache-Status, X-Original-Generated-At and X-Runtime helps to determine if the cache is functioning properly. When statisfied, it's safe to remove those lines in production code.

Conclusion

These ~100 lines of php will saves you a lot a bits.

Update

Google Code LogoI created a new project on google code to host the code of my PHP CSS minifier. Along with a complete rewrite in OO PHP, you will be able to download the latest sources from the CSS Shrink project.

Spread the word:
  • del.icio.us
  • Reddit
  • StumbleUpon
  • Technorati
  • Twitter
  • DZone
  • Facebook
  • FriendFeed
  • HackerNews

19 Responses to “CSS Minification on the Fly”

  1. Corey Ballou 21 January 2010 at 5:34 pm Permalink

    I’m not whole-heartedly believing that a difference of 2kb caused load time to decrease from ~2200ms to ~320ms. How many times did you run your tests? Did you make sure to clear your cache before running each test? I’d recommend including your testing environment as well as the number of tests run to achieve your averages.

    • Nicolas Crovatti 23 January 2010 at 5:59 pm Permalink

      I’ll make some more tests and publish them as soon as I get enough time.

  2. Inside the Webb 23 January 2010 at 2:31 am Permalink

    Cool article man! I’ll have to try some of these techniques with the CSS files on Inside the Webb, it would speed up my page loads dramatically.

  3. MaxiWheat 23 January 2010 at 4:15 am Permalink

    We have done something like that on our side, but I don’t think we went as far as you with the caching. One step further would be to concatenate all css files into only one file which would reduce the number of http requests and latency. Doing that you also need to parse css files for @import statements to include them too in the concatenated file.

    • Nicolas Crovatti 23 January 2010 at 5:57 pm Permalink

      Hi MaxiWheat,

      I did not find a way to avoid the multiple HTTP requests with this method yet. There’s great tools that are doing that very well, but they are requiring a totaly different initial setup (http://code.google.com/p/minify/).

      Concerning @imports my technic catches them all because of the rewrite rules.

  4. Shayne 23 January 2010 at 6:29 am Permalink

    Not to mention the fact that nearly every webserver out there runs some sort of on-the-fly compression already which comes close to the same savings that you get by running cpu intensive find/replaces.

    • Nicolas Crovatti 23 January 2010 at 5:53 pm Permalink

      You probably are referring to Gzip compression I guess. The minification technic explained in this article is a step further into the file shrinking methods. Don’t forget we are talking about optimization here.

      Nowadays bandwidth is far more expensive than CPU time. So, once in 24 hours computations of half a dozen CSS files should not even be noticed on any servers.

  5. Vsync 23 January 2010 at 2:15 pm Permalink

    Corey Ballou & Shayne are absolutely right. I’m very skeptical about your method.

  6. AntonioCS 24 January 2010 at 11:33 am Permalink

    Please disable magic quotes and add syntax highlight to your code!!

    • Nicolas Crovatti 24 January 2010 at 5:04 pm Permalink

      Oh, sorry! The first snippet have magic quotes enabled ! I’ll correct this asap. However may I suggest you to disregard the first snippet to the profit of the later one which is far more complete ?

  7. elena 24 January 2010 at 3:41 pm Permalink

    It was interesting to read this article and I hope to read a new article about this subject in your site in the near time.

  8. Mike 26 January 2010 at 12:18 am Permalink

    I did something similar with an Apache mod_perl based solution. It intercepts any outgoing text/css responses and compresses them on the fly:

    https://secure.grepular.com/Compressing_CSS_on_the_Fly

    You can minify JavaScript too. See here:

    https://secure.grepular.com/Compressing_JavaScript_on_the_Fly

  9. mjc 26 January 2010 at 1:42 am Permalink

    You must be running a low-to-moderate traffic site, passing every css load through php seems to be pretty wasteful of CPU cycles, not to mention spending even more to minify it first when you clearly have someone waiting on your page.

    If you really want to do it this way, it might be smart to send the file plain, and start the minification to a cache file after the client already has all the CSS. This would give the first request the CSS as fast as possible (to negate the delay required while compressing and writing a new file), and every new request afterward the minified version.

    In some cases I’ve done this and stuffed the result into memcache, then had nginx try to retrieve from memcache first… but most of the time it pays to just minify it on commit and toss it on a CDN.

    • Nicolas Crovatti 26 January 2010 at 7:40 am Permalink

      Hello mjc,

      I agree on the pre-process minification of the assets and served from a CDN.

      But, did you read about the caching part ? This is quite negating the compression + writing delay.

      According to my tests, the user waiting for my page to load will suffer a 0.05 sec load time when the css file he is requesting is not cached (once in 24 hours for only one user, the unlucky one) and about 0.0003 sec when the file is cached (the rest of the time for all other users the lucky ones).

      This is the average on my current server configuration: Intel Celeron 220 @ 1.20GHz with 2Gb RAM. Which is kind of a low end server.

      One enhancement I can see tho, is to send a 304 HTTP code if the client have a valid copy in its cache.

  10. Adrian Ramiro 27 January 2010 at 3:11 am Permalink

    Thanks for sharing this. I think it’s a good solution for not ‘compiling/minifing’ CSS everytime we edit them and keep readable files.

    Just one thing, in the second example:

    You call:
    23.echo compress($content);

    While you’ve defined:
    31.function minify($data)

    A little versioning trouble :P

  11. tedivm 27 January 2010 at 3:31 am Permalink

    You may be interested in the JShrink class, a php based tool that minifies javascript. I wrote it last month and have been working with the minify people (among others) to enhance its functionality.

    http://code.google.com/p/jshrink-/

    I’ve also found that apache’s compression can potentially be slower than a well setup php script. By caching the compressed version you can bypass that whole step on subsequent requests, while apache itself will run that compression each time.

    • Nicolas Crovatti 27 January 2010 at 9:52 am Permalink

      I will definitly take a look at it tedivm. And you are completly right about the Apache compression too. I did not even though about it.

      Nice finding!