Update
Introduction
Most sites include a number of CSS and JavaScript files. Whilst developing it's usually easier to manage them as separate files but on a live site it makes sense to merge files to reduce the number of HTTP requests the browser has to make. For JavaScript this is particularly important as browsers block rendering whilst downloading. It's also important to version your files to ensure that browsers download the latest copies when you've made changes.
I hate maintaining this stuff manually so I've written a PHP script which takes care of merging files on the fly whilst also versioning the merged file automatically as the various component files change. The file is merged on first request and cached. Subsequent requests are served the cached version. The script also sets HTTP headers to ensure the user's browser maintains each version in its own local cache therefore preventing repeated requests to the server. Finally an archive of the merged files is maintained to ensure that requests for old versions return the relevant CSS/JavaScript rather than the latest which might not match the user's cached HTML.
Using the script
Step 1: Start by setting the correct mime type for the files you want to merge.
define('FILE_TYPE', 'text/javascript');
Step 2: Modify the $aFiles array to include the paths to the files you want to merge. These should be relative to the server document root.
$aFiles = array('js/yahoo.js','js/event.js','js/connection.js','js/blog-search.js');
Step 3: Set the location the script should write the archive files to. When first run it will automatically create the folder you specify if it doesn't already exist. For this to work you'll need to make sure that the parent directory, in this case "js", is owned (or is writable) by the user your web server runs as.
define('ARCHIVE_FOLDER', 'js/archive');
Step 4: When called directly the script returns the merged code which you reference from your HTML source. For JavaScript files your HTML source should look something like this:
<script type="text/javascript" src="/js/site_<?php require('combine.php'); ?>.js"></script>
When included via require the script returns the latest version number rather than the source. When rendered it will look like this:
<script type="text/javascript" src="/js/site_1166088093.js"></script>
I've used a .htaccess file containing the following mod_rewrite rules to map this filename to the script.
RewriteEngine OnRewriteBase /RewriteRule js/site_([0-9]+).js js/combine.php?version=$1 [L]
If your host doesn't support .htaccess files you can rewrite your code to:
<script type="text/javascript" src="/js/combine.php?version=<?php require('combine.php'); ?>"></script>
That's it for the set up. When you make changes to your source files the script will now take care of updating both the code served and the corresponding filename in the HTML source.
Caveats
If you subsequently add files to the script which have older last-modified dates than those already included they won't trigger a new version. I could have added code to support this but it would have significantly increased the complexity of the script. To trigger a new version simply touch or re-save one of the files.Thanks
Thanks to Mike Davies for suggesting the archive functionality and for generally acting as a great sounding board.
Further Information
The following links were really helpful whilst putting together this script.
- HTTP/1.1: Header Field Definitions
- Ned Martin - Caching
- Supporting Conditional GET in PHP
- HTTP Conditional Get for RSS Hackers
- Keeping your Dynamic Pages Dynamic
- Caching Tutorial
- Cacheability Engine
Feedback
The script is still very much work in progress. I'd really like to hear what you think of it - suggestions for improvements, problems etc.
I really like this idea. Perhaps a future version could automatically run the js files through JSMin after combining them, so as a developer you only have to work with non-minimised code.
Ed -- Great work on this. Thanks for sharing it. This goes right in hand with what Tenni Theurer talks about in her 80/20 Rule on Performance article on YUIBlog the other day. Regards, -Eric
Nice ideas! Great! Fnx!
You might be interested in the work we did on a related technique last summer:
http://bestpractical.typepad.com/worst_impractical/.../smart_caching_f.html
Stuart and I discussed the idea of integrating JS Min this afternoon. I think it probably makes most sense to try and modify the PHP version to allow it to be called as an include rather than via the command line. I'll try and get something working over the next few days.
Eric - thanks for your feedback and also taking the time to add a comment to the performance article on YUI blog pointing back to my post.
Jesse - thanks for the link. I haven't had time to fully digest yet - I'll be taking a closer look.
This looks nice, Ill have to test it out later. I did something similar for a previous project to reduce requests to the server. Slightly different approach - but still achieving the same goal.
Nice work.
Thanks for the script. This is exactly what I was looking for to reduce the number of javascript files from having to be downloaded. Thanks again. :)
Very cool, thanks!
Just as a wee sugar on top, you could replace this part: // files to merge $aFiles = array( 'js/yahoo.js', 'js/event.js', 'js/connection.js', 'js/blog-search.js' );
by: $aFiles = glob('js/*.js');
Dieter - I considered doing that but there could be situations where the order that the JS or CSS files are merged in is important. If you merge with glob you can't be sure of this.
If you have PHP prior to 4.3, :( then replace line 68 with
$sCode .= implode('',file("$sDocRoot/$sFile"));
file_get_contents() is 4.3.0 or later.
thanks for this nifty script!
thats good ,, it have usefull script that contain some good ideas.. i think it will help me.. continue...
Thank you! Looking forward to giving this a try.
Good stuff. It seems like, in a high performance situation, checking the files' last-modified dates on every page request might be an unnecessary performance hit. What about creating a cron job to determine the last-modified date periodically and putting into APC cache or something similar? That way the script can just check the cached value rather than calculating it every time.
I've created a WP plugin that optimizes CSS fully automatically. You might want to check it out. It can be found in this article I wrote about front end optimisation.
Modern browsers multithreaded, I do not see this problem
Just as a wee sugar on top, you could replace this part: // files to merge $aFiles = array( 'js/yahoo.js', 'js/event.js', 'js/connection.js', 'js/blog-search.js' );
by: $aFiles = glob('js/*.js');
+1
How can i use it with html?
Great script. I'm very impressed. It's given me some great ideas on how to break up my CSS and JS into logical chunks when developing but still have a larger streamlined version in production.
The only problem I found was that explicitly setting Content-Length causes delays when the web server is using gzip, compress, deflate, etc. The browser is expecting the uncompressed length, and sits and waits for more data even though the server delivered it already in a compressed form. Watching response times in FireBug, it would always take 6+ seconds to deliver one combined JS file. Removing the Content-Length got it back down into the millisecond range.
Thanks again.
My suggestions: a. I want to run a server side code (perl in my case) only when the script is not compressed:
ErrorDocument 404 /cache
b. I want to make (ideally) only one request per page 1 for css, one for js): /assets/cache/css/one.css,two.css,three.css (I found this interesting technique, the joining line in my thought, here: http://rakaz.nl/item/make_your_pages_load_faster_by_combining_and_compressing_javascript_and_css_files)
In this way I can run server side code only when necessary (Apache gives 404), and when the script runs: - it splits by comma - if not already done, it compresses each single file and store it under "cache" - it put all together the compressed files into a compressed big file containing commas: next request will not have to run the script!
What are your impressions? Expires condition are missing yet, I promise I'll work harder ;-)
http://rakaz.nl/item/make_your_pages_load_faster_by_combining_and_compressing_javascript_and_css_files
"Modern browsers multithreaded, I do not see this problem"
Yes, but modern browsers still only use one thread for downloading JS-files and halt page rendering or downloading of other assets while doing so :)
this helped me in how to break up my CSS and JS into logical chunks..thanks very much , have a nice day....Nice site