Check Out CSS Cacheer

Update: CSS Cacheer has been updated to v003. This update fixes a bug in the Nested Selectors plugin that prevented stylesheets containing quotes (eg. content:".";) from being successfully parsed. If you’ve already downloaded a copy to play with be sure to grab to the latest version.

While playing about with the data: protocol and base64 encoding to embed images in CSS (thereby cutting down on the number of requests made for additional files) I hit upon an idea: what if the server could detect capable browsers and automatically embed any images referenced in the CSS it was serving? Like some kind of, I don’t know, server-side CSS pre-processor?

Oh right, we already did that. Despite having an acronym long enough to be phone number, CSS-SSPP has some cool features. Like server-side imports, variables, constants, bases and the coolly-received nested-selectors. It even takes care of caching processed stylesheets. A few people talked of extending it but nothing ever came of that; it just wasn’t built with extensibility in mind.

So rather than build yet another pre-processor, I gutted CSS-SSPP, making it more extensible and improving caching. With plugins providing the pre-processing, I’ve dubbed the new mini-application CSS Cacheer. Caching now includes support for certain plugin-defined variables and, if supported, gzipping of the cached file.

In addition to the previously mentioned features from CSS-SSPP (with some syntactic updates I’ll outline below) I’ve also created a plugin that, you guessed it, automatically embeds any image referenced in the CSS file for capable browsers.

Cache Course

CSS Cacheer requires PHP and Apache (with mod_deflate and mod_rewrite) and is, as always, offered as is. ShaunInman.com has been using the Condenser and Base64 plugins for a couple days now with no problems to report but your mileage may vary.

Download the archive and unzip. Upload the following directory and two files to your existing style directory:

  • .htaccess
  • css-cacheer/
  • css-cacheer.php

Change the permissions of the nested css-cacheer/cache/ directory to 777. The included example files are not required but provide a quick overview of the syntax of each of the included plugins.

Plugins are stored in css-cacheer/plugins/. By default all but the Condenser plugin are enabled so you can play with the example files. Plugins with filenames prefixed by a - are disabled and not loaded by CSS Cacheer. Enable plugins by removing the leading -.

Developers, Developers, Developers, Developers…

Developing plugins for CSS Cacheer is relatively simple. Anyone familiar with PHP who can copy/paste and cobble together a useful regular expression can do it.

Plugins have 3 methods: pre_process for importing and simple replacements, process for the heavy lifting and post_process for formatters. Each method is passed a string containing the contents of the requested CSS file and each must return a processed version of that string. All plugins should extend CacheerPlugin. If a plugin does not make use of one of the methods, it shouldn’t implement it. Just let inheritance do its job.

Plugins that do something conditionally (eg. based on browser capabilities or time of day) must set a flag via the plugins flags array property so that the outcome of different conditions can be cached individually. Check out the source of the included Base64 plugin for an example of how this is done.

Plugins are loaded in alphabetical order according to the server file system. Processing order can be loosely controlled by overriding the appropriate method. More granular control will be provided in a future update if required.

Syntacticly Delicious

Server-side importing remains the same:

@server import url(examples/variables-et-all.css);

Constants have been updated to more closely match the CSS Variables proposal and eliminate the single constant syntax:

/* defining constants */
@constants
{
    constantName: constantValue;
}

/* using constant values */
selector
{
    propertyName: const(constantName);
}

Variables have been eliminated as of CSS Cacheer v002 because the variable caching mechanism created a denial of service attack vulnerability.

Based-on only changes in the way you define a base:

@base(baseName)
{
    propertyName: propertyValue;
    propertyName: propertyValue;
    propertyName: propertyValue;
}

Retrieving those property/value pairs remains the same:

selector
{
    based-on: base(baseName);
}

Nested selectors work as before:

ul
{
    propertyName1: propertyValue1;

    li
    {
        propertyName2: propertyValue2;
    }
}

Still outputs:

ul
{
    propertyName1: propertyValue1;
}

ul li
{
    propertyName2: propertyValue2;
}

But now correctly ignores @media rules like:

@media screen and (-webkit-min-device-pixel-ratio:0)
{
    selector
    {
        propertyName: propertyValue;
    }
}

The Base64 image embedding plugin doesn’t require any special syntax. Just specify the path to your background image as you normally would and it does the rest:

selector
{
    background: url(path/to/img.gif) 0 0 no-repeat;
}

Over to you

I have created a couple plugins but I certainly haven’t exhausted all possibilities. In the comments for CSS-SSPP Rob mentioned basic math and boolean evaluation. I’ve been kicking around an idea for a plugin that defines the :if(), :elseif(), and :else pseudo-selectors along with some globals to conditionally output styles depending the page or directory requesting the CSS. Let’s see what you can come up with!

The current version of CSS Cacheer (v003) has been downloaded 1870 times.

Previous
The Biggest Drawing in the World
Next
DevKick
Author
Shaun Inman
Posted
May 30th, 2008 at 12:34 pm
Categories
CSS
PHP
CSS-SSC
Comments
014 (Now closed)

014 Comments

001

Shucks Shaun, this is pretty sweet! I can’t wait to take it for a spin. It addresses a number of issues that my friends and I had discussed in passing this year at SXSW, where we theorized on what an object-oriented extension of CSS should look like. We started with simply wanting to declare variables, but pretty soon wanted to extend classes, similar to what “based-on” does.

Author
Dane
Posted
May 30th, 2008 1:43 pm
002

Pretty slick, Shaun! Can’t wait to have a project to work with :-)

I would like to see more docs, though. That would be useful, I’m sure you do agree, but I also understand you don’t have much time for it anyway.

Author
Jerome Verzier
Posted
May 30th, 2008 3:03 pm
003

Really sorry for the double post, I wanted to correct the few mistaking requests I made: the doc is currently available with some minor modifications though, like the new syntax var(variableName) and const(constantName), and not to forget the new additions :-)

Keep up the extremely well work, Shaun, you’re our everyday dev’ hero!

Author
Jerome Verzier
Posted
May 30th, 2008 3:22 pm
004

You’re trying to cut down on the amount of queries for static images to Apache by introducing PHP code that runs a bunch of regular expressions, a base 64 encode, and is dependent upon mod_rewrite, one of the most expensive (in CPU time) Apache modules?

Why?

Just by looking at your package, without even looking at your Apache configuration, I can already tell you that you’re developing a solution for people who don’t have Apache configured correctly in the first place.

If you have AllowOverride set to anything but “None”, Apache is going to perform a stat() on .htaccess down the entire path all the way to the directory in question. Let’s assume you have this set up at /var/www/html/csscacheer. For every request for a static image in that directory, Apache will then perform a stat() on /.htaccess to see if it exists, /var/.htaccess, /var/www/.htaccess, /var/www/html/.htaccess and finally /var/www/html/csscacheer/.htaccess. That’s five extra disk operations for every image you serve up.

One single-core Celeron, with Apache correctly configured and tuned, can pump out 45Mbps, easily. If you’re running into issues where you feel the need to reduce the amount of queries going into Apache, you have a lot of tuning that you need to do, especially before trying to cobble together a solution based upon extremely expensive CPU operations such as base64 or a regex.

Author
Mark
Posted
May 30th, 2008 3:39 pm
005

Do you have any recommendations for creating and maintaining documentation? I’m not a fan of phpDoc which is typically harder to understand than actual source code. And I don’t think it really applies to documenting CSS syntax additions supported by CSS Cacheer plugins.

Something as simple as a wiki with Markdown support might be all that’s needed.

That said, most of the necessary documentation is present in this post. Did I miss something?

Author
Shaun Inman
Posted
May 30th, 2008 3:41 pm
006

I should add: this isn’t meant to be disparaging in any way, or take away from your innovation in any way. I’m sure this will be extremely useful for certain situations where clients need to have data urls embedded inside the original html page. (Extremely-timely, dynamic, security/antiphishing issues come to mind, like a bank’s image verification service.)

However, for day-to-day usage, it’s far easier to have Apache deal with serving up static files, something it is very very good at, rather than trying to wrestle with an application-level solution.

Author
Mark
Posted
May 30th, 2008 3:47 pm
007

Mark, that’s why it caches processed CSS files (it doesn’t touch HTML). It only performs the embedding once.

Putting that oversight aside, you obviously know a lot about how to properly configure Apache. Rather than popping in just to say, “you’re doing it wrong”, why not share some of that knowledge so we can all be as smart as you.

Also, Apache isn’t the only (or even the targeted) bottleneck here. Fewer external file requests (and accompanying, compounded network latency) is better for browser rendering time as well. Especially when optimizing a site for mobile devices where latency can be especially high.

Author
Shaun Inman
Posted
May 30th, 2008 3:49 pm
008

You’re doing it wrong!

In all seriousness, though, looking through your code, there are a few places where you’re going to cause a bottleneck.

First:

!file_exists(substr($requested_file, strlen($css_dir) + 1))

Here’s where you perform an extra stat() on the file, even after Apache has already performed that stat() in the original request. This is a relatively expensive operation. If all you’re concerned about is embedding base64 data inside your file, then you should be doing that at file creation, not at runtime. Have whatever CMS you’re using embed the data instead.

Second:

$cached_file = $cssc_cache_dir.preg_replace('#(.+)(.css)$#i', "$1-{$checksum}$2", $relative_file);

Based on this line alone, someone armed with knowledge that you were running this script could perform a denial of service attack on your webserver fairly easily. Just make sure the md5 sum of the GET request changes every time. That’s trivial to do.

(Ed. variables and $_GET-based caching have been disabled as of CSS Cacheer v002)

Third:

$requested_mod_time = filemtime($relative_file);

You’re running another stat() here on the requested file, and the next line runs a stat() on a “cached” file. These may be on the order of microseconds, but they add up as additional overhead that could be better spent streaming data to the client from Apache.

Fourth:

ob_start('ob_gzhandler');

Never never never never do this in PHP, JSP, or any other application you’re writing! Add this:

<IfModule mod_deflate.c>
    AddType text/css .your-cached-file-extension
    AddOutputFilterByType DEFLATE text/html text/css application/x-javascript text/plain text/xml
</IfModule>

…and let Apache take care of the gzip compression for you. Even if you don’t want Apache to do that, at least do the gzip compression the first time you write out the cached file, rather than doing it over and over, again, at runtime.

Last, color me a little skeptical about render time differences vis a vis decoding base64 that’s directly in a stylesheet vs. simply another pipelined request. I don’t even know how you would begin to reliably test that; any speed differences would be extremely small, and could be impacted by some background process on the client running at the wrong time, etc.

Again, Shaun, this is a neat hack, don’t get me wrong. It can be very useful in the right place, I’m sure.

Author
Mark
Posted
May 30th, 2008 5:07 pm
009

Very cool stuff.

I’m not entirely sold on the base64 encoding of images through. By lumping your images in with the rest of your CSS, won’t the initial page load be significantly longer?

As I understand it, isn’t the downloading & parsing/interpreting of CSS a serial operation by the browser? The rest of the page load has to wait until the CSS rules are finished. And then all of the images for the page can be downloaded in parallel.

Here’s how my site loads, according to FireBug: http://dev.robgoodlatte.com/loadTimes.png

The CSS files “block” the other transfers from happening. Then, once the CSS is finished, images can load in parallel.

If you encode your images into the CSS, won’t that serial CSS operation take much longer?

Nevertheless, really interesting. And I can definitely see advantages in a mobile space.

Author
Rob Goodlatte
Posted
May 30th, 2008 6:19 pm
010

Do you have any info about how using data urls affects the effectiveness of gzip? Can you achieve better overall served filesize by having it all in one file or by using seperate files and therefore seperate compression schemes?

Author
Andrew Ingram
Posted
May 30th, 2008 6:20 pm
011

Damn it, I had written a thorough reply that responded to each of your points all typed up in Safari. But for some reason, choosing Develop > Open Page With > Firefox 2 in the menu refreshed the tab blasting my reply. So take the following with a grain of salt; I don’t have the patience to retype right now.

Mark et al, draw your own conclusions about the utility of CSS Cacheer. I will be disabling support for variables to prevent potential for denial of service attacks.

Author
Shaun Inman
Posted
May 31st, 2008 10:15 am
012

I’m a total n00b when it comes to this stuff but wondering out loud: Would you accomplish much of what you want to do with cacheer (which is a very cool concept) by using CSS Sprites? You reduce your http requests to a single image request which (as pointed out above) is something Apache does very well.

Move that image out of an “AllowOverride” enabled directory to remove the extra stat() (or many, depending on directory depth) and the time it takes to parse the .htaccess file if one exists and Apache will happily serve like mad. and if httpd is part of the bottleneck, consider “nginx” as an alternative to Apache — It’ll be like your first crush in High School all over again.

What I really don’t know is how that affects render time on the browser level. Perhaps the trade-off is not worth it but if Sprites can be rendered quickly and efficiently, it may be worth considering.

Remember: I’m lame when it comes to CSS and have no idea how to benchmark browser render times in different scenarios outside of Firebug output.

Cacheer still looks like a neat way to burn a few hours tinkering.

Author
Tom
Posted
May 31st, 2008 12:00 pm
013

Sure Shaun, a wiki would be a great solution for your documentation, letting us do our part of the job aswell as letting you concentrate on making greater things again and again.

That also would be a much better way for users to ask for features or warn you of bugs.

I am looking forward to collaborate on this wiki :)

Author
Jerome Verzier
Posted
May 31st, 2008 5:32 pm
014

I think a wiki or setting up a forum would be a great idea to help you collaborate with others and store information on bugs and specific topics.

As for CSS Cacher, do you plan on creating a directory of plug-ins for it?

Author
Janet
Posted
Jun 1st, 2008 5:10 am