Skip to: Site menu | Main content

Case Study: Increasing YSlow Score 30+ Points in Less Than One Hour

Ever since a client of mine asked me to look into the Firebug add-on YSlow, I've been interested in using it to increase performance on my Drupal (version 5) web sites. Wim Leer's recent posting about improving Drupal performance inspired me to take action to see what kind of improvements I could make.

Before I get into the details, please don't confuse me with an Apache guru. I know enough to modify various settings in an httpd.conf or an .htaccess file, but only after I've done my due diligence to make sure I'm not going to irrevocably screw things up.

While the title of this post indicates that I made all the changes in under an hour, in reality, it took me several hours to do all the research and testing. I'm hopeful that with the work I've done, other Drupal site admins will be able to make similar changes in under an hour.

I decided to focus on the low-hanging fruit. Since my site is small, I'm not looking to implement using a content delivery network (CDN). I'm also wanted to see what I can do with little or no changes to the site's code (other than the .htaccess file). With that in mind, I decided to look at the following categories:

  • Make fewer HTTP requests
  • Add an Expires header
  • GZip components
  • Minify JS
  • Configure ETags

By improving these 5 categories, I was able to increase's YSlow score from an "F" (53) to a "B" (85).

Here's exactly what I did:

Turned on "Aggregate and compress CSS files"

This was a no-brainer and something that I had failed to do when I intially launched the site. As part of Drupal core (admin/settings/performance), it basically combines all of your site's various CSS files into a single file and compresses it (mainly by removing white space). Doing this reduces the number of files and the file size of your CSS definitions that the user has to download. This brought my YSlow score up from 53 to 60.

Configure "FileETag none"

According to Yahoo!'s Developer Network blog "Entity Tags (ETags) are a mechanism that web servers and browsers use to determine whether the component in the browser's cache matches the one on the origin server." The article goes on to say that unless you're explicity using them, it's best to just turn them off. You can do this by adding the following line in your .htaccess file:

FileETag none


Be warned, I found a couple of "top-o-the-search-page" references for ETags that have this definition incorrect (I'm looking at you, sitepoint). They indicate that the definition is "FileETags none" (note the plurality) - it took me a little while to figure out why this kept causing a "server not available" message on my site. Drupal 6 does not appear to include this definintion in the default .htaccess file. Configuring this brought my YSlow score up from 60 to 64.

Added far future Expires headers

This one was a little tricky for me, so I took a bit of conservative approach. The idea behind Expires headers is that when your server sends content (HTML, images, CSS, etc...) to a user's browser, it can tag each piece of content with an expiration date. This is ideal if you have a photo of you and your cat on your server that isn't going to be modified anytime soon. If you set the expiration date for that photo to be 2 years from now, then the next time the user visits your site (providing it is within 2 years and they haven't cleared out their browser's cache), they won't have to download the photo again, the browser will just pull it from its cache.

By default, Drupal has the following line in the .htaccess file:

ExpiresByType text/html A1

This basically says that for any file with the MIME-type of text/html, set the expiration date to 1 second past the current time of the user's computer ("A" indicates the user's computer, "1" indicates the number of seconds).

What I decided to do was to set the expiration date of some other MIME-types to 2 years in the future. So, this is what I modified the entire mod_expires block to:

ExpiresActive On
ExpiresByType text/html A1
ExpiresByType application/x-javascript "access plus 2 years"
ExpiresByType application/javascript "access plus 2 years"
ExpiresByType text/javascript "access plus 2 years"
ExpiresByType text/css "access plus 2 years"
ExpiresByType image/gif "access plus 2 years"
ExpiresByType image/jpeg "access plus 2 years"
ExpiresByType image/jpg "access plus 2 years"
ExpiresByType image/png "access plus 2 years"

The "text/html" line remains unchanges, but I added the "ExpiresActive On" line above it. This just ensures that the Expires header stuff is turned "on" (I believe it is "on" by default, but this line doesn't hurt). I also added a number of additional ExpiresByType lines to set far-future expiration dates for javascript, css, and image files.

There is was way to set the expiration date for all content at once, but as I said earlier, I decided to take a more conservative approach. Drupal 6 looks to take a slightly different approach - the .htaccess file that is slated to ship sets all files other than text/html to expire 2 weeks from their first access.

The only "danger" of this that I can figure is that when you update a file, if you don't change its filename, then a user who has it in their cache may not get the updated file. This might be especially important for updated CSS and JavaScript files when a theme or module is upgraded. However, I think this might be mitigated by the fact that the the CSS aggregation and the javascript_aggregator module (see below) both create aggregate files named by an MD5 hash, thus continually generate a new filename. Making this change to my .htaccess file brought my YSlow score from 64 to 79 - a whopping 15 points!

GZip components

Instead of a server sending a browser a plain uncompressed HTML text file, wouldn't it make more sense if it could be compressed on-the-fly by the server and uncompressed by the browser? Not surprisingly, this is (or should be) the standard way of doing things. Apache doesn't do this by default, but if you're using version 2.0, it's pretty easy to configure. All I did was add the following lines to my .htaccess file:

AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml application/x-javascript application/json
Header append Vary Accept-Encoding

The first line tells the server to "DEFLATE" (compress) any HTML, CSS, TXT, XML, JavaScript, or JSON data before sending it to the browser. Since most image formats like GIF, JPG, and PNG are already compressed, there's no reason to add them to the list. The second line helps ensure that the server doesn't serve GZipped components to browsers that don't support it. Drupal 6 does not address GZipping components in its .htaccess file. If you're using Apache 1.x, then check out the previously mentioned sitepoint article for several options. Making this change brought my YSlow score from 79 to 84.

javascript_aggregator module

Finally, I decided to see what modules were out there to do for JavaScript what the CSS aggregator does for style sheets. The javascript_aggregator module does just about the exact same thing. Like the CSS Aggregator, it combines and compresses your site's JavaScript files in a bid to both reduce the number of files that must be downloaded as well as their overall size. The javascript_aggregator compresses JavaScript files only by removing comments and white-space. It doesn't "compress" JavaScript files like Dean Edward's most-excellent Packer, but there's talk of adding that to Drupal 7.

Installing the javascript_aggregator module is simple enough, just enable the module, then go to admin/settings/performance to turn it on and to configure its options. It allows you to specify particular JavaScript files that the aggregator should ignore (apparently TinyMCE's JavaScript doesn't play nicely with others). Installation of the module also requires a small, but easy change to your theme's page.tpl.php file. Enabling this module improved my YSlow score from 84 to 85, a solid "B".

I hope this information is useful, please let me know of your own results in the comments below!

Submitted by michael on Wed, 02/06/2008 - 3:19pm
Filed under:

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

More woes (with JS aggregator)

Despite the Gzip problems I explained in the previous comment, I forged ahead today with the Javascript aggregator module. It was no help. It caused JavaScript errors in the combined script, despite my excluding tinyMCE and jquery from the aggregation. So I'm leaving it out.

That leaves the expires headers. YSlow seems to think 3 days is enough to be in the "far future," but 2 days is not. I'm afraid to set the expiration very far ahead for anything, especially JS and CSS, because there doesn't seem to be a simple solution for those occasions when you need to change the files. Using a different file name apparently is the most common choice, but implementing that reliably and elegantly requires creating a whole build process and a method of removing old versions of files. Any suggestions on that?

Gzip woes

Before I read your very helpful article, I turned on mod_gzip for various file types on our server, including html, js and css. It reduces the content length by as much as 70%, but we got a lot of errors in the Apache log like this:

[error] mod_gzip: TRANSMIT_ERROR:32
[error] mod_gzip: TRANSMIT_ERROR:104
[error] mod_gzip: TRANSMIT_ERROR:ISMEM:104

Most of what I read on this is not helpful. It just says the clients have terminated the connection. I am guessing that some clients send a request header that says they accept gzip encoding, but then they reject it. A colleague speculated that corporate firewalls cause this problem. (We know corporate security often rejects .zip file attachments in email, but that seems quite different from zipped content on http port 80.)

Does anybody know the cause of these errors? I'm afraid to use gzip without knowing who can no longer get our content.

Alternative for gzipping on the fly

Milan is right: gzipping everything on the fly will add a heavy overload to your server. I did it at my cuentos infantiles site some time ago, and the result was my hosting costs increased. I looked for a solution to this, and this is the result: use cached versions of your files previously gzipped
To achieve this:
- for pages: just use the built in cache option for drupal
- for css and javascript files: use the smartcache module, indeed a very smart way to gzip your files, wich is very straightforward to install.
I used this at the site I mentioned, and worked perfectly, while maintaing cpu usage MUCH lower.
One last performance tip I also applied to my cuentos site: if most of your users are anonymous, the boost module will enhance your performance to the top. Maybe Yslow does not reflect it, but your page will not only load faster, but you would be able to serve far more pages per second

not sure

Well, that I don't know. If it does indeed, that would be great! But as far as I know this would be the realm of a cache mechanism like APC or even above on an apache-basis. I'm not sure this is default behavior.

good points

Thanks for your comments - good points. Some questions though: 1. Doesn't Apache GZip then cache everything so it isn't constantly GZipping? 2. Thanks for the info on jQuery. -mike

1. gzipping everything looks

1. gzipping everything looks fine and might improve your YSlow ranking but all it does is that it makes your bandwidth bill smaller. Once you get slashdotted/dugg etc. your site will brake sooner rather than later! Because now your server has to gzip every reply, which costs extra cpu circles. This will mean it can serve less site/minute. Thus I'm pretty sure this feature should not be activated by default. 2. JS compression: You might know jquery: It has not only the packed version via the mentioned Packer utility. It also gives you gzipped and minified: Not sure if you meant that in your blog post. That's it, good bye.