views:

2998

answers:

5

I have a few sites on a shared host that is running Apache 2. I would like to compress the HTML, CSS and Javascript that is delivered to the browser. The host has disabled mod_deflate and mod_gzip, so these options are out. I do have PHP 5 at my disposal, though, so I could use the gzip component of that.

I am currently placing the following in my .htaccess file:

php_value output_handler ob_gzhandler

However, this only compresses the HTML and leaves out the CSS and JS.

Is there a reliable way of transparently compressing the output of the CSS and JS without having to change every page? I have searched Google and a number of solutions are presented, but I've yet to get one to work. If anyone could suggest a solution that they know to work, that would be very gratefully received.

Note, Method 2 in The Definitive Post on Gzipping your CSS looks like a good solution, but I couldn't get it working. Has anyone else succeeded using this method?

+1  A: 

You can try your luck with mod_rewrite.

Create a script that takes a local static file name as input, through e.g. $_SERVER['QUERY_STRING'] and outputs it in compressed form. Many providers don't allow configuring mod_rewrite with .htaccess files or have it completely disabled though.

If you haven't used rewrite before, I recommend a good beginner's guide, like probably this one. This way you can make the apache redirect all requests for a static file to a php script. style.css will be redirected to compress.php?style.css for instance.

As always be extremely cautious on the input you accept or you have an XSS exploit on your hands!

macbirdie
@macbirdie, many thanks for this suggestion. Can you provide, or link to, any examples of this approach? I'd also be interested in hearing some general techniques on securing such a script.
Charles Roper
+3  A: 

What I do:

  • I place scripts in a js and stylesheets in a css dir, respectively.
  • In the Apache configuration, I add directives like so:

    <Directory /data/www/path/to/some/site/js/>
     AddHandler application/x-httpd-php .js
     php_value auto_prepend_file gzip-js.php
     php_flag zlib.output_compression On
    </Directory>
    <Directory /data/www/path/to/some/site/css/>
     AddHandler application/x-httpd-php .css
     php_value auto_prepend_file gzip-css.php
     php_flag zlib.output_compression On
    </Directory>
    
  • gzip-js.php in the js directory looks like this:

    <?php
     header("Content-type: text/javascript; charset: UTF-8");
    ?>
    
  • …and gzip-cs.php in the css directory looks like this:

    <?php
     header("Content-type: text/css; charset: UTF-8");
    ?>
    

This may not be the most elegant solution, but it most certainly is a simple one that requires few changes and works well.

Sören Kuklau
I've just tried this and I couldn't get it to work unfortunately. When I add the directives as suggested, I get an Internal Server Error.
Charles Roper
Me too..any ideas on what I can do to fix that? This seems like a great solution
Coughlin
+1  A: 

what ever you do, be careful about caching on the client side:

Browsers do all sort of tricks to try and minimize the bandwith and there are many ways in the HTTP protocol to do that, all of which are dealt with by apache - if you are just serving a local file.

If you are not, then it's your responsibility.

Have a look at least at the ETag and the If-Modified-Since mechanics which are supported by all current browsers and seem to be the most robust way to query the server for updated content.

A possible way to serve a CSS file to browsers using the If-Modified-Since-Header is something like this (the empty headers to turn off any non-caching headers PHP sends per default):

$p = 'path/to/css/file'
$i = stat($p);
if ($_SERVER['HTTP_IF_MODIFIED_SINCE']){
    $imd = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
    if ( ($imd > 0) && ($imd >= $i['mtime'])){
        header('HTTP/1.0 304 Not Modified');
        header('Expires:');
        header('Cache-Control:');
        header('Last-Modified: '.date('r', $i['mtime']));
        exit;
    }
}
header('Last-Modified: '.date('r', $i['mtime']));
header('Content-Type: text/css');
header('Content-Length: '.filesize($p));
header('Cache-Control:');
header('Pragma:');
header('Expires:');
readfile($p);

The code will use the if-modified-since-header the browser sends to check if the actual file on the server has changed since the date the browser has given. If so, the file is sent, otherwise, a 304 Not Modified is returned and the browser does not have to re-download the whole content (and if it's intelligent enough, it keeps the parsed CSS around in memory too).

There is another mechanic involving the server sending a unique ETag-Header for each piece of content. The Client will send that back using an If-None-Match header allowing the server to decide not only on the date of last modification but also on the content itself.

This just makes the code more complicated though, so I have left it out. FF, IE and Opera (probably Safari too) all send the If-Modified-Since header when they receive content with a Last-Modified header attached, so this works fine.

Also keep in mind that certain versions of IE (or the JScript-Runtime it uses) still have problems with GZIP-transferred content.

Oh. And I know that's not part of the question, but so does Acrobat in some versions. I've had cases and cases of white screens while serving PDFs with gzip transfer encoding.

pilif
+4  A: 

Sorry about the delay - it's a busy week for me.

Assumptions:

  • .htaccess is in the same file as compress.php
  • static files to be compressed are in static subdirectory

I started my solution from setting the following directives in .htaccess:

RewriteEngine on
RewriteRule ^static/.+\.(js|ico|gif|jpg|jpeg|png|css|swf)$ compress.php [NC]

It's required that your provider allows you to override mod_rewrite options in .htaccess files. Then the compress.php file itself can look like this:

<?php

$basedir = realpath( dirname($_SERVER['SCRIPT_FILENAME']) );
$file = realpath( $basedir . $_SERVER["REQUEST_URI"] );

if( !file_exists($file) && strpos($file, $basedir) === 0 ) {
    header("HTTP/1.0 404 Not Found");
    print "File does not exist.";
    exit();
}

$components = split('\.', basename($file));
$extension = strtolower( array_pop($components) );

switch($extension)
{
    case 'css':
     $mime = "text/css";
     break;
    default:
     $mime = "text/plain";
}

header( "Content-Type: " . $mime );
readfile($file);

You should of course add more mime types to the switch statement. I didn't want to make the solution dependant on the pecl fileinfo extension or any other magical mime type detecting libraries - this is the simplest approach.

As for securing the script - I do a translation to a real path in the file system so no hacked '../../../etc/passwd' or other shellscript file paths don't go through.

That's the

$basedir = realpath( dirname($_SERVER['SCRIPT_FILENAME']) );
$file = realpath( $basedir . $_SERVER["REQUEST_URI"] );

snippet. Although I'm pretty sure most of the paths that are in other hierarchy than $basedir will get handled by the Apache before they even reach the script.

Also I check if the resulting path is inside the script's directory tree. Add the headers for cache control as pilif suggested and you should have a working solution to your problem.

macbirdie
Thanks, I will give this a go. I sure wish SOF had email alerts to tell you when a question is updated.
Charles Roper
Hey, with this how would I use it. I save this as compress.php correct? Do i use an include?
Coughlin
This script assumes it's run by the apache Rewrite rule and is given a filename as an URI parameter.
macbirdie
I got it. But if I update my files it doesnt work. DOes this mean the old ones are compressed? I updated my CSS and I get a page with content but no styling.
Coughlin
What do you mean by updating the files? You don't have to do anything with the files that are to be compressed - you just have to put this script on a server and set up the Rewrite rules appropriately.
macbirdie
+1  A: 

Instead of gzipping on the fly when users request the CSS and JavaScript files, you could gzip them ahead of time. As long as Apache serves them with the right headers, you’re golden.

For example, on Mac OS X, gzipping a file on the command line is as easy as:

gzip -c styles.css > styles-gzip.css

Might not be the sort of workflow that works for you though.

Paul D. Waite