Background
A recent YSlow 2.0 presentation posted on Slideshare and an old post from Greg Linden which resurfaced recently highlight the issue of webpage performance. Google found that half a second delay caused a 20% drop in traffic. Amazon's own research indicated a 1% drop in sales for 100ms delay while Yahoo! discovered a 5-9% drop in traffic when a 400ms slowdown was introduced.
I've been working on a PHP asset bundler recently to try and improve webpage load times inline with recommendations from the Yahoo! Exceptional Performance team. I first came across Tim Harper's Bundle-fu for Ruby on Rails a year or so ago and it was this that first prompted me to look into this subject in depth.
If you want to read more about this then I can recommend JavaScript Performance Rocks by Thomas Fuchs and Amy Hoy. Yes, it costs money but it represents good value considering the contained wisdom gleaned from the first-hand experience of developing one of the most popular and robust JavaScript libraries.
Requirements
PHP 5: This code was developed using PHP 5.2.6 things may work as expected with versions as low as 5.1.0 but don't hold me to that.
YUI Compressor: If you want to use the 'minify' option to compress your generated bundles then you'll also need to download the YUI Compressor and have a Java interpreter (e.g. java or gij) available.
Download
bundle-php.zip [6KB]
The ZIP includes the bundle class, the example below and a cache directory with an example .htaccess file with mod_deflate and mod_expires options.
Usage
You first need to edit bundle.class.php in order to set the location of the cache directory where you wish to store your bundles. If you're wanting to minify your bundles then you'll also need to set the full paths to your Java interpreter and the location of the Yahoo! Compressor. Look at lines number 49 and 50.
After you've finished editing, include bundle.class.php at the top of your code and then set up an output buffer to bundle up your files as shown below (setting any options as you wish). Don't forget to flush your buffers and the very bottom of your page.
<?php
include 'bundle.class.php';
function bundleUp($buffer) {
$args = array(
phpcreated => true,
autoregen => false,
embed => true,
);
$bundle = new Bundler($args);
return $bundle->loadpage($buffer);
}
ob_start('bundleUp');
// Your code goes here
. . .
ob_end_flush();
?>
Make sure that you have the appropriate write permissions set on your cache directory to allow the bundles to be saved once they have been generated. There will obviously be something of a hit the first time that the bundles are generated for a page but you could always do this yourself on your development environment before deploying to your live server instead of leaving it to your visitors!
These cached files can safely be served with far future expires headers as the filename will change depending on the contents. I'm trying to figure out a smart way of cleaning up previous versions of bundles but it's very much a manual process for now I'm afraid.
Options
- minify (default false)
- Using this option will also generate a minified version of the bundles to use. You must have the YUI Compressor available to use this option (see requirements above).
- reflow (default true)
- This will restructure the webpage to place JavaScript at the bottom (directly before the closing
</body>tag) and style sheets at the top (immediately after the closing</title>tag) of your webpage. - dynamic (default false)
- If, like Wordpress for example, your JavaScript or CSS is part-generated by PHP or some other server-side language and must be served over HTTP then set this option to true. Omitting this option or setting it as false will load your files over the filesystem instead.
- autoregen (default true)
- Setting this option to true will make bundles refresh themselves should any of the files or code snippets be updated.
- embed (default false)
- A lot of sites have many small decorative CSS background images to use as bullets for example. These can be embedded in the CSS directly as data URIs which will save one HTTP request per image. Data URIs are roughly 33% larger than the filesize of the corresponding image but this is largely offset if you gzip your content. Note that the Internet Explorer browser only supports data URIs as of version 8 so a non-embedded CSS file will be served to earlier versions of IE. Take into consideration the number of visitors to your site that use a non-capable browsers before setting this option to true.
- localonly (default false)
- If you don't want to include JavaScript and CSS files from another domain in your bundles then set this option to true. Be careful when using this with the reflow option as you may encounter dependency issues.
Results
I ran five tests of loading static HTML, a bundled version of the same HTML and also a bundled version with CSS background images embedded. The HTML was actually one of my blog posts as Wordpress does scatter a fair amount of JavaScript and CSS in the page markup if you have even a couple of plugins installed. I saved the source in order to prevent any Wordpress engine issues obscuring the data.
The tests were performed using Firefox 3 and timed with the LORI extension. The browser cache was cleared after every test. No other extensions, tabs or programs were enabled, open or running during the tests.
| Document (gzip) | Images | Style Sheets (gzip) | Scripts (gzip) | Total (gzip) | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Normal | 1 | 27.3K (6.9K) | 9 | 10.4K | 2 | 29.3K (8.9K) | 11 | 253.3K (80.8K) | 23 | 320.3K (107K) |
| Bundle | 1 | 24.3K (6.2K) | 9 | 10.4K | 1 | 22.7K (5.4K) | 1 | 149.8K (48.9K) | 12 | 207.2K (70.9K) |
| +Embed | 1 | 24.3K (6.2K) | 2 | 5.8K | 1 | 42.5K (14.7K) | 1 | 149.8K (48.9K) | 5 | 222.4K (75.6K) |
There is an obvious correlation between making more requests and a longer time to page completion.
| Run | First byte (s) | Completion (s) | ||||
|---|---|---|---|---|---|---|
| Normal | Bundle | +Embed | Normal | Bundle | +Embed | |
| 1 | 0.446 | 0.399 | 0.370 | 8.088 | 4.478 | 3.883 |
| 2 | 0.350 | 0.386 | 0.677 | 6.268 | 3.421 | 1.859 |
| 3 | 0.401 | 0.374 | 0.421 | 9.486 | 3.217 | 4.352 |
| 4 | 0.502 | 0.399 | 0.588 | 5.653 | 2.597 | 3.159 |
| 5 | 0.393 | 0.355 | 0.439 | 8.009 | 3.415 | 2.852 |
| Average | 0.422 | 0.383 | 0.500 | 7.501 | 3.426 | 3.221 |
While the time taken to receive the first byte only varies by less than 100 milliseconds but the bundling strategies consistently achieved at least a three second reduction in complete page load time. With a primed cache the page loads in under a second.
Contact
If you have any questions, suggestions or other comments on Bundle PHP then please feel free to send me an email.
History
v0.4 19/02/09
- Add an option to only bundle local files
- Fixed a problem where print style sheets would also be bundled up which could result in strange behaviour
- Fix an issue with concatenating JavaScript includes
v0.3 13/02/09
- Fix a fault in the bundle naming scheme where inline code was ignored
- Fix greedy regular expression matching
- Add the ability to disable bundling during development using the querystring:
bundling=off
v0.2 09/02/09
- Add the option to embed small CSS background images as data URIs for capable browsers
- Only include any repeated file once per bundle
- Rebase CSS images to be root relative
- Fix an issue where any JavaScript errors would break minification
v0.1 31/01/09
- Initial release
