Minify CSS on the fly

This post is more than 13 years old.

Keeping your website content as small as it can be so that it downloads fast is as important today as it was back in the pre-broadband days, given that you don’t know whether your visitors will be accessing your websites via broadband, dialup, or mobile connections. As CSS files grow, it can be as important to minify them to trim the fat as it is for JavaScript downloads. Here’s how I do it on-the-fly for CSS.

Firstly, there are some good tools for minifying static CSS files so that your pages can link directly to them. This is best, because it doesn’t add any extra load to the server. My favourite one is the YUI Compressor, which I use for static CSS and JavaScript. I’ve bound the F9 function key in Geany, my editor of choice, to run YUI compressor for CSS and JavaScript files, so it’s literally a one-keypress operation for me.

However, CSS is often managed by non-programmers, and adding extra steps into the development chain can be met with… resistance :) so I’ve built a little script to minify the CSS on-the-fly. This lets CSS writers concentrate on what they do best, without sending bloated comment-heavy CSS to the web browser.

Static CSS minifiers can afford to examine every aspect of CSS code to come up with the smallest possible CSS. They can do that because there’s really nobody waiting for the file except the CSS writer. But it’s different with on-the-fly minification, where time and server processing resources should be kept to a minimum. Thus, my script targets only the big-ticket and quickest items in CSS minification:

  • comments, easily the biggest piece in well-written CSS (you do write comments, yes?)
  • whitespace (but only the whitespace that doesn’t alter CSS behavior!)
  • semicolons preceding closing braces (i.e. ;} where the semicolon is optional)

While I’m at it, I figure I might as well add some extra benefits though:

  • concatenate multiple CSS files into a single download, reducing server requests
  • select different sets of source files, e.g. desktop vs mobile
  • gzip compression to further reduce download size, quite dramatically
  • cache control to tell browsers not to request the CSS again for a while

So, to the code.

/**
* send one or more files as determined by an action parameter,
* with compression if accepted by browser
*
*      e.g. scripts.php?a=css&v=3
*/

// set this to FALSE to disable CSS minify
$cssMinify = TRUE;

/**
* function to perform simple, conservative minification of CSS, without too much processor hit
* @param string buffered CSS output
* @return string minified CSS output
*/
function my_ob_css_minify($css) {

    // remove comments
    $css = preg_replace('@/*.*?*/@s', '', $css);

    // remove leading whitespace from start of CSS
    $css = preg_replace('@^s+@', '', $css);

    // remove unnecessary leading whitespace, using look-ahead assertion
    $css = preg_replace('@s+(?=[!{};,>])@', '', $css);

    // remove unnecessary trailing whitespace, using look-behind assertion
    $css = preg_replace('@(?<=[!{};,>])s+@', '', $css);

    // remove unnecessary ; before }
    $css = str_replace(';}', '}', $css);

    return $css;
}

// make sure we're being called with action parameter
if (!isset($_GET['a']))
    die('no scripts specified.');

// start output buffering with gzip, if available
ob_start("ob_gzhandler");

// set some reasonable expiry time so that the browser will cache the output 
// and not ask for it again for a while
// NB: use GET parameters to ensure new versions will be downloaded, 
//     e.g. scripts.php?a=css&v=3
$expires = 60 * 60 * 24 * 7; // 7 days
header("Cache-Control: max-age=$expires");
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');

// determine which set of files to send
switch($_GET['a']) {
    // CSS files for desktop website, media = all
    case 'css':
        $files = array('style.css', 'bg.css', 'menu.css', 'menu-two.css', 'print.css');
        if ($cssMinify)
            ob_start('my_ob_css_minify');
        header('Content-Type: text/css; charset: UTF-8');
        break;

    // CSS files for mobile website, media = all (effectively same as above but without background images)
    case 'css-m':
        $files = array('style.css', 'menu.css', 'menu-two.css', 'print.css');
        if ($cssMinify)
            ob_start('my_ob_css_minify');
        header('Content-Type: text/css; charset: UTF-8');
        break;

    // mobile CSS delivered either by media query or media = handheld,all
    case 'css-mobile':
        $files = array('mobile.css');
        if ($cssMinify)
            ob_start('my_ob_css_minify');
        header('Content-Type: text/css; charset: UTF-8');
        break;

    default:
        // NOP
        $files = array();
        break;
}

// read each file into the output buffer
foreach ($files as $file) {
    readfile($file);
}

// output buffers will be emptied at end of script...

So, how much difference does it make? Here’s what it does on a relatively complex website we’re working on right now:

Rawgzipminifyminify+gzip
68kb11kb56kb9kb

As you can see, the biggest impact is made by gzip compressing the file, but minifying squeezes an extra couple of kb off the download. That’s barely noticeable for a broadband user, but on a slow mobile connection with packet loss it might still be appreciated! And the difference will get bigger as the CSS writer adds more comments(!)

There, job is done faster.