CSS minifier in PHP

A while ago, I was trying to find a way to reduce the number of HTTP requests made on my pages. To make development easier, each section of the CSS is in a different file; this way, I know exactly where everything is located when I want to make changes. However, making 10 to 15 request just to get the CSS is too much overhead. It could be much better if I could combine them all into one.

This is how my template that contained the CSS looked like:

...
<link href="<?php echo ASSET_CDN?>/css/reset.css?<?php echo RELEASE_VERSION?>" type="text/css" rel="stylesheet" /> 
<link href="<?php echo ASSET_CDN?>/css/style1.css?<?php echo RELEASE_VERSION?>" type="text/css" rel="stylesheet" />
<link href="<?php echo ASSET_CDN?>/css/style2.css?<?php echo RELEASE_VERSION?>" type="text/css" rel="stylesheet" />
<link href="<?php echo ASSET_CDN?>/css/style3.css?<?php echo RELEASE_VERSION?>" type="text/css" rel="stylesheet" />
...

Since all the CSS is separated, it keeps it more organized and easier to access. However, on production I don't want to make all these requests. There are a few solution I had in mind.

Before doing anything, let's write our CSS minifier.

Minifying CSS with PHP.

Minifying consist of removing unnecessary whitespace and comments from the document. Thankfully, CSS syntax is not so complicated and we can minify it without breaking the code.

In my minifier, I used just 2 steps:

Removing whitespace

Here is a function that does just that with Regex and string replace.

function remove_spaces($string){
    $string = preg_replace("/\s{2,}/", " ", $string);
    $string = str_replace("\n", "", $string);
    $string = str_replace(', ', ",", $string);
    return $string;
}

In the first line of the function, we use preg_replace to look for any consecutive 2 or more white spaces \s{2,} and replace them with a single space. Then we remove newlines. When a style is applied to multiple elements a comma is used, more often then not it is followed by a space that can also be removed. The results will be a long string with no line breaks.

Next we strip the comments out.

Removing comments

Luckily, CSS only has one type of comments /* Comment */ Which is much easier to deal with. One simple regex can help us remove them.

function remove_css_comments($css){
    $file = preg_replace("/(\/\*[\w\'\s\r\n\*\+\,\"\-\.]*\*\/)/", "", $css);
    return $file;
}

The Regex looks for everything that starts with a /* followed by words, spaces, new lines, quotes, dashes and ends with a */. In my particular case, I added +,-.. You can add more characters to suit your needs.

Now that we have a function that can remove whitespace and one that removes comments, we can easily minify and combine all our CSS in one file. Here is the complete code:

/**
 * A very simple css Minifier
 */
class CSSmini {

    /**
     * Array of CSS files path
     **/
    private $cssPath = array();


    /**
     * Initialize the class, optionally set the array of css paths
     **/
    public function __construct($cssFilesPath = array()){
        if (is_array($cssFilesPath) && !empty($cssFilesPath)){
            $this->cssPath = $cssFilesPath;
        }
    }

    /**
     * Add the path of a css file to stack.
     * @param file of CSS file
     **/
    public function addFile($cssFilePath){
        $this->cssPath[] = $cssFilePath;
    }

    /**
     * Minify the current css files added.
     * If no css path is set throws error and exit.
     **/
    public function minify(){
        $allCss = array();
        if (empty($this->cssPath)){
            echo "No CSS was added";
            exit; // maybe you will have a better error handler
        }

        foreach($this->cssPath as $css){
            // make sure its a css file
            $bits = explode(".",$css);
            $extention = $bits[count($bits)-1];
            if ($extention !== "css") {
                echo "Only CSS allowed";
                exit; // or better error handling
            }
            $file = file_get_contents($css);
            $file = $this->remove_spaces($file);
            $file = $this->remove_css_comments($file);
            $allCss[] = $file;
        }

        return implode("\n",$allCss);
    }

    /**
     * Remove unnecessary spaces from a css string
     * @param String $string
     * @return String
     **/
    private function remove_spaces($string){
        $string = preg_replace("/\s{2,}/", " ", $string);
        $string = str_replace("\n", "", $string);
        $string = str_replace('@CHARSET "UTF-8";', "", $string);
        $string = str_replace(', ', ",", $string);
        return $string;
    }

    /**
     * Remove all comments from css string
     * @param String $css
     * @return String
     **/
    private function remove_css_comments($css){
        $file = preg_replace("/(\/\*[\w\'\s\r\n\*\+\,\"\-\.]*\*\/)/", "", $css);
        return $file;
    }
}

The class and methods are self explanatory. Here is an example of it being used:

$minifier = new CSSmini();

// add the list of css files
$minifier->addFile("http://example.org/css/reset.css");
$minifier->addFile("http://example.org/css/style1.css");
$minifier->addFile("http://example.org/css/style2.css");

// Merge and minify all the files
$css = $minifier->minify();

// Save the file
file_put_contents("path/to/css/style.min.css",$css);

Note:

You can run it during deployment. This way, in the development process you can still work on the well organized files and only combine them in production.

Serving the files to users

Now that we have a method to minify our CSS, what are the options to serve it very fast to the user?

Using Nginx

I use Apache as a web server. Nginx is better predisposed to serve static files. All the CSS could be served on a different server or CDN to alleviate the load on the main Apache server handling requests. Even though it is an improvement, there is still the problem of having multiple requests. It can still be slow for some users.

Combining files on the fly

I can have a script whose sole purpose is to minify and combine files on the fly. I could pass it the file names in the URL and it combines it automatically.

http://example.org/minifier.php?type=css&files=reset.css|style1.css|style2.css|style3.css

$files = explode("|",$_GET['files']);

$minifier = new CSSmini($files);

header("Content-type: text/css");
echo $minifier->minify();
exit;

This could work too, however I would have to use a caching mechanism. Re-minifying on every request would be a waste of resources. The options could be a combination of browser cache, memcached, and or Varnish.

Update: I recently wrote about a method for serving files with PHP as fast as a webserver.

Generating a static CSS file

The options I went with is generating a combined minified version of all the CSS files and saving it as one single file. I wrote a script to read the CSS tags on my page and replace it with a single <link> tag. The first thing to do was to add place holders to make it easier to parse the file.

Here is how the template looks like with the place holders:

// maintemplate.html.php
<!doctype html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title><?php echo $view->getSlot("title")?></title>
    <?php echo $view->getMeta();?>
    <link rel="shortcut icon" href="<?php echo ASSET_CDN?>/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
<?php // <Remove>?>
    <link href="<?php echo ASSET_CDN?>/css/reset.css?<?php echo RELEASE_VERSION?>" type="text/css" rel="stylesheet" /> 
    <link href="<?php echo ASSET_CDN?>/css/style1.css?<?php echo RELEASE_VERSION?>" type="text/css" rel="stylesheet" />
    <link href="<?php echo ASSET_CDN?>/css/style2.css?<?php echo RELEASE_VERSION?>" type="text/css" rel="stylesheet" />
    <link href="<?php echo ASSET_CDN?>/css/style3.css?<?php echo RELEASE_VERSION?>" type="text/css" rel="stylesheet" />
<?php // </Remove>?>
    <!-- #MIN-CSS -->
    <link type="text/plain" rel="author" href="/humans.txt" />
    ...

Note: The CSS tags are still on the page because I don't want to combine them while I am developing. It is much harder to debug a style on a minified CSS.

I used the tag <?php // <Remove>?> <?php // </Remove>?> as a place holder to identify the section I want to remove on production. <!-- #MIN-CSS --> will be the placeholder for the the new minified CSS.

To replace the place holders, I used a naive method to simply find the position of those tags and remove them.

$template = file_get_contents("maintemplate.html.php");
$start = strpos($template, '<?php // <Remove>');
$end = strpos($template, '<?php // </Remove>')+21; // <?php // </Remove>')?> is 21 characters long
$section = substr($template, $start, $end - $start);
$template = str_replace($section, "", $template);

What this code does is find where <?php // <Remove>?> <?php // </Remove>?> is on the page and remove it. To add the minified css, we can use a string replace to get the job done.

$template = str_replace('<!-- #MIN-CSS -->', '<link href="<?php echo ASSET_CDN?>/css/style.min.css" rel="stylesheet" type="text/css" />', $template);

Then we can use file_put_contents() to save maintemplate.html.php. Here is how maintemplate.html.php looks like after the place holders have been processed.

// maintemplate.html.php
<!doctype html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title><?php echo $view->getSlot("title")?></title>
    <?php echo $view->getMeta();?>
    <link rel="shortcut icon" href="<?php echo ASSET_CDN?>/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link href="<?php echo ASSET_CDN?>/css/style.min.css" rel="stylesheet" type="text/css" />
    <link type="text/plain" rel="author" href="/humans.txt" />
    ...
</head>

We can serve this static minified file from the CDN if we want to.

In summary:

We opened our template and replaced the placeholder with a path to the minified version of the CSS.

One last optional step

Reducing the requests to a single on is already a great improvement. There are rare cases when the CDN or whatever you use to serve your files stalls and blocks the rendering of the page. The browser already downloaded the content of the page but can't render because the assets are still being downloaded. This is when you can use the style tag with inline styles.

It is usually not recommended to use the style tag because you will mix content and layout. This is correct,but it is only true during development. Our templates and our style sheet is already organized, it is only during deployment that we include the CSS into the page. I used to care how my page source looks like but remember that your HTML is for the browser and bots, we are humans after all and we read text and enjoy images.

Let's update the first script to include the CSS into a <style> instead of a <link> instead:

$template = str_replace('<!-- #MIN-CSS -->', '<style type="text/css"><?php include '/path/to/css/style.min.css'?></style>', $template);

Now we have zero requests made for our CSS. Your content should now render blazingly fast, with no time wasted making extra request to CSS resources.

minified css included on the page

Page source with minified css included. Who cares how it looks like.

I hope you learned how to minify your CSS. But more importantly, I hope you understand why we minify. We want to make the page load as fast as possible, and reducing the requests to the minimum possible. In our case, we reduced it to zero.

Thank you for reading.


Comments

Bien Thuy :

It seem there is a problem with function remove_css_comment Could you please check it. It show error on " character

ibrahim :

@BienThuy I just checked the function, it seems to be working fine on my side. Make sure when you are copying your browser is not changing the double quotes " into a ”

furiousgold.com :

The CSSmini class has a huge security hole that needs a fix. It doesn't check the file extension. So, I could download your entire website with it.

Ibrahim :

Good catch @furiousgold, thank you for letting me know I have updated the code. One thing I will add eventually is to limit the files to those in the css folder.

Let's hear your thoughts

For my eyes only