Tue, 31 Jul 2007

Rails performance tip - using YSlow

Posted by Ben Tue, 31 Jul 2007 20:50:18 GMT

YSlow from Yahoo! is a Firefox add-on to analyse web pages and tell you why they’re slow based on rules for high performance web sites. YSlow requires the indispensable Firebug extension.

The 13 rules YSlow checks your site against are as follows:

1. Make Fewer HTTP Requests
2. Use a Content Delivery Network
3. Add an Expires Header
4. Gzip Components
5. Put CSS at the Top
6. Move Scripts to the Bottom
7. Avoid CSS Expressions
8. Make JavaScript and CSS External
9. Reduce DNS Lookups
10. Minify JavaScript
11. Avoid Redirects
12. Remove Duplicate Scripts
13. Configure ETags

This post will demonstrate that most of these are easily achievable for a Rails website through a combination of plugins and with correct configuration of a proxy web server (in front of a mongrel cluster) – in this case Nginx. This guide follows experience with improving performance for trawlr.com (an online RSS reader).

Make Fewer HTTP Requests, Minify JavaScript, Put CSS at the Top, Move Scripts to the Bottom, Remove Duplicate Scripts

The easiest way to make fewer HTTP requests is to combine all JavaScript and CSS files into one. The asset packager plugin does exactly this, plus it will also compress the source files (in production mode) and correctly handles caching (without query string parameters).

Moving CSS to the top (within the head section) and moving JavaScript to the bottom of the page are both manual tasks that should be done in the layout templates (such as app/views/layouts/application.rhtml). Remember to use stylesheet_link_merged :base and javascript_include_merged :base rather than the default Rails helpers.

By using asset packager you can also verify that scripts are only included once – another performance hit otherwise!

Excluding the Google analytics JavaScript file, trawlr.com now uses a single css and js file (including the entire prototype library). Note: You may need to add a missing semi-colon as per this defect for prototype to work correctly.

Asset Packager can be included as part of a Capistrano deployment with the following recipe:

desc "Compress JavaScript and CSS files using asset_packager" 
task :after_update_code, :roles => [:web] do
  run <<-EOF
    cd #{release_path} &&
    rake RAILS_ENV=production asset:packager:build_all
  EOF
end

Use a Content Delivery Network

Ignoring this point for now; I’d suggest the use of Amazon S3 as a useful starting point for simple CDN.

Add an Expires Header

A first-time visitor to your page may have to make several HTTP requests, but by using the Expires header you make those components cacheable. This avoids unnecessary HTTP requests on subsequent page views. Expires headers are most often used with images, but they should be used on all components including scripts, stylesheets, and Flash components.

Nginx allows adding arbitrary HTTP headers via the expire and add_header directives. Adding the expires header to static content is done with a regular expression looking for relevant file extensions in the request URL. This example uses the maximum expiry date but could be set to more appropriate values as required (e.g. 24h, 7d, 1M)

# Add expires header for static content
location ~* \.(js|css|jpg|jpeg|gif|png)$ {
  if (-f $request_filename) {
        expires      max;
    break; 
  }        
}

Gzip Components

Nginx can gzip any responses – including those proxied from a mongrel cluster.

gzip on;
gzip_min_length  1100;
gzip_buffers     4 8k;
gzip_proxied any;              
gzip_types  text/plain text/html text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;

Avoid CSS Expressions

Just don’t do it!

Make JavaScript and CSS External

Add you JavaScript and CSS styles in external files rather than inline. The added benefit here is that the content will be merged and compressed thanks to the work already done above.

Reduce DNS Lookups, Avoid Redirects, Configure ETags

These weren’t an issue for me so I suggest the Yahoo! guidance for further information

Reduce DNS Lookups

Avoid Redirects

Configure ETags

Summary

After making the changes outlined above the YSlow score for trawlr.com has hit a B grade (89) with all points A grade except “Use a CDN” which I have not addressed. The “Stats” view indicates that with an empty browser cache there would be 30 HTTP requests (26.0K total size), with a full cache this drops to a single request (the HTML document) (6.5K total size). Worth the effort in my opinion!

Comments

Leave a response

  1. Paul M. Watson about 8 hours later:

    Thanks, good guide.

    On the CDN front I am pretty sure S3 isn’t a CDN as it is at most in two locations. Hopefully S3 starts offering a CDN option though.

  2. Jake Stetser about 18 hours later:

    Technically, you can ‘cheat’ with the CDN rule – if you spread your accesses out over several domains (with something like asset_hosts), there’s an about:config setting in which you can list your own domains for the CDN.

    I did that for now since I think only akamai and yahoo are currently listed in their CDN list.

  3. The Smartass Who Ran YSlow on This Site 8 days later:

    URL: http://www.slashdotdash.net/articles/2007/07/31/rails-performance-tip-using-yslow

    Performance Grade: D (63)

    B 1. Make fewer HTTP requests

    This page has 5 external JavaScript files.

    This page has 3 external StyleSheets.

    F 2. Use a CDN These components are not on a CDN:

    • [HTTP headers] http://www.slashdotdash.net/.../application.css?... * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:18 GMT Content-Type: text/css Content-Length: 895 Last-Modified: Tue, 07 Aug 2007 00:01:34 GMT Accept-Ranges: bytes Loading…
    • [HTTP headers] http://www.slashdotdash.net/.../layout.css * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:19 GMT Content-Type: text/css Content-Length: 1089 Last-Modified: Tue, 07 Aug 2007 00:01:35 GMT Accept-Ranges: bytes Loading…
    • [HTTP headers] http://www.slashdotdash.net/.../content.css * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:19 GMT Content-Type: text/css Last-Modified: Tue, 07 Aug 2007 00:01:35 GMT Content-Encoding: gzip Loading…
    • [HTTP headers] http://www.slashdotdash.net/.../cookies.js?... * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:17 GMT Content-Type: application/x-javascript Last-Modified: Thu, 03 Aug 2006 21:45:23 GMT Content-Encoding: gzip Loading…
    • [HTTP headers] http://www.slashdotdash.net/.../prototype.js?... * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:18 GMT Content-Type: application/x-javascript Last-Modified: Thu, 03 Aug 2006 21:45:23 GMT Content-Encoding: gzip Loading…
    • [HTTP headers] http://www.slashdotdash.net/.../effects.js?... * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:18 GMT Content-Type: application/x-javascript Last-Modified: Thu, 03 Aug 2006 21:45:23 GMT Content-Encoding: gzip Loading…
    • [HTTP headers] http://www.slashdotdash.net/.../typo.js?... * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:18 GMT Content-Type: application/x-javascript Last-Modified: Thu, 03 Aug 2006 21:45:23 GMT Content-Encoding: gzip Loading…
    • [HTTP headers] http://www.google-analytics.com/urchin.js * ParamsHeadersPost Response Headers Cache-Control: max-age=604800, public Content-Type: text/javascript Last-Modified: Mon, 18 Jun 2007 22:56:31 GMT Content-Encoding: gzip Server: ucfe Content-Length: 6232 Date: Tue, 07 Aug 2007 21:48:37 GMT Loading…
    • [HTTP headers] http://www.slashdotdash.net/.../background.gif * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:11:15 GMT Content-Type: image/gif Content-Length: 2740 Last-Modified: Tue, 07 Aug 2007 00:01:35 GMT Connection: keep-alive Accept-Ranges: bytes Loading…
    • [HTTP headers] http://www.slashdotdash.net/.../header_shadow.gif * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:11:15 GMT Content-Type: image/gif Content-Length: 87 Last-Modified: Tue, 07 Aug 2007 00:01:35 GMT Connection: keep-alive Accept-Ranges: bytes Loading…
    • [HTTP headers] http://www.slashdotdash.net/.../spinner.gif?... * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:11:15 GMT Content-Type: image/gif Content-Length: 1553 Last-Modified: Tue, 07 Aug 2007 00:01:34 GMT Connection: keep-alive Accept-Ranges: bytes Loading…
    • [HTTP headers] http://feeds.feedburner.com/.../slashdotdash?... * ParamsHeadersPost Response Headers Date: Wed, 08 Aug 2007 18:11:14 GMT Server: Apache X-FB-Host: app64 Expires: Wed, 08 Aug 2007 20:11:14 GMT Cache-Control: max-age=7200 Content-Length: 425 P3P: CP=”ALL DSP COR NID CUR OUR NOR” Keep-Alive: timeout=300 Connection: Keep-Alive Content-Type: image/gif Set-Cookie: NSC_gffe-iuuq-mc-wtfswfs=8efb33623660;expires=Wed, 08-Aug-07 19:14:18 GMT;path=/ Loading…

    You can add your own CDN hostname preferences.

    F 3. Add an Expires header These components do not have a far future Expires header:

    • [HTTP headers] (no expires) http://www.slashdotdash.net/stylesheets/theme/application.css?1186444894 * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:18 GMT Content-Type: text/css Content-Length: 895 Last-Modified: Tue, 07 Aug 2007 00:01:34 GMT Accept-Ranges: bytes Loading…
    • [HTTP headers] (no expires) http://www.slashdotdash.net/stylesheets/theme/layout.css * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:19 GMT Content-Type: text/css Content-Length: 1089 Last-Modified: Tue, 07 Aug 2007 00:01:35 GMT Accept-Ranges: bytes Loading…
    • [HTTP headers] (no expires) http://www.slashdotdash.net/stylesheets/theme/content.css * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:19 GMT Content-Type: text/css Last-Modified: Tue, 07 Aug 2007 00:01:35 GMT Content-Encoding: gzip Loading…
    • [HTTP headers] (no expires) http://www.slashdotdash.net/javascripts/cookies.js?1154641523 * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:17 GMT Content-Type: application/x-javascript Last-Modified: Thu, 03 Aug 2006 21:45:23 GMT Content-Encoding: gzip Loading…
    • [HTTP headers] (no expires) http://www.slashdotdash.net/javascripts/prototype.js?1154641523 * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:18 GMT Content-Type: application/x-javascript Last-Modified: Thu, 03 Aug 2006 21:45:23 GMT Content-Encoding: gzip Loading…
    • [HTTP headers] (no expires) http://www.slashdotdash.net/javascripts/effects.js?1154641523 * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:18 GMT Content-Type: application/x-javascript Last-Modified: Thu, 03 Aug 2006 21:45:23 GMT Content-Encoding: gzip Loading…
    • [HTTP headers] (no expires) http://www.slashdotdash.net/javascripts/typo.js?1154641523 * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:09:18 GMT Content-Type: application/x-javascript Last-Modified: Thu, 03 Aug 2006 21:45:23 GMT Content-Encoding: gzip Loading…
    • [HTTP headers] (no expires) http://www.slashdotdash.net/images/theme/spinner.gif?1186444894 * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:11:15 GMT Content-Type: image/gif Content-Length: 1553 Last-Modified: Tue, 07 Aug 2007 00:01:34 GMT Connection: keep-alive Accept-Ranges: bytes Loading…
    • [HTTP headers] (8/8/2007) http://feeds.feedburner.com/~fc/slashdotdash?bg=99CCFF&;fg=444444&anim=0 * ParamsHeadersPost Response Headers Date: Wed, 08 Aug 2007 18:11:14 GMT Server: Apache X-FB-Host: app64 Expires: Wed, 08 Aug 2007 20:11:14 GMT Cache-Control: max-age=7200 Content-Length: 425 P3P: CP=”ALL DSP COR NID CUR OUR NOR” Keep-Alive: timeout=300 Connection: Keep-Alive Content-Type: image/gif Set-Cookie: NSC_gffe-iuuq-mc-wtfswfs=8efb33623660;expires=Wed, 08-Aug-07 19:14:18 GMT;path=/ Loading…
    • [HTTP headers] (no expires) http://www.slashdotdash.net/images/theme/background.gif * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:11:15 GMT Content-Type: image/gif Content-Length: 2740 Last-Modified: Tue, 07 Aug 2007 00:01:35 GMT Connection: keep-alive Accept-Ranges: bytes Loading…
    • [HTTP headers] (no expires) http://www.slashdotdash.net/images/theme/header_shadow.gif * ParamsHeadersPost Response Headers Server: nginx/0.5.14 Date: Wed, 08 Aug 2007 18:11:15 GMT Content-Type: image/gif Content-Length: 87 Last-Modified: Tue, 07 Aug 2007 00:01:35 GMT Connection: keep-alive Accept-Ranges: bytes Loading…

    C 4. Gzip components These components are not gzipped:

    C 5. Put CSS at the top 2 external stylesheets were found outside the document HEAD.

    C 6. Move scripts to the bottom 4 external scripts were found in the document HEAD. Could they be moved lower in the page?

    • [HTTP headers] http://www.slashdotdash.net/javascripts/cookies.js?1154641523 * ParamsHeadersPost Response Headers Cache-Control: max-age=604800, public Content-Type: text/javascript Last-Modified: Mon, 18 Jun 2007 22:56:31 GMT Content-Encoding: gzip Server: ucfe Content-Length: 6232 Date: Tue, 07 Aug 2007 21:48:37 GMT Loading…
    • [HTTP headers] http://www.slashdotdash.net/javascripts/prototype.js?1154641523 * ParamsHeadersPost Response Headers Cache-Control: max-age=604800, public Content-Type: text/javascript Last-Modified: Mon, 18 Jun 2007 22:56:31 GMT Content-Encoding: gzip Server: ucfe Content-Length: 6232 Date: Tue, 07 Aug 2007 21:48:37 GMT Loading…
    • [HTTP headers] http://www.slashdotdash.net/javascripts/effects.js?1154641523 * ParamsHeadersPost Response Headers Cache-Control: max-age=604800, public Content-Type: text/javascript Last-Modified: Mon, 18 Jun 2007 22:56:31 GMT Content-Encoding: gzip Server: ucfe Content-Length: 6232 Date: Tue, 07 Aug 2007 21:48:37 GMT Loading…
    • [HTTP headers] http://www.slashdotdash.net/javascripts/typo.js?1154641523 * ParamsHeadersPost Response Headers Cache-Control: max-age=604800, public Content-Type: text/javascript Last-Modified: Mon, 18 Jun 2007 22:56:31 GMT Content-Encoding: gzip Server: ucfe Content-Length: 6232 Date: Tue, 07 Aug 2007 21:48:37 GMT Loading…

    A 7. Avoid CSS expressions n/a 8. Make JS and CSS external Only consider this if your property is a common user home page. A 9. Reduce DNS lookups C 10. Minify JS The following JavaScript files do not appear to be obfuscated nor minified.

    A 11. Avoid redirects A 12. Remove duplicate scripts A 13. Configure ETags

  4. Ben 8 days later:

    @The Smartass Who Ran YSlow on This Site

    Ha, I never even tried it on this site… I’m in the process of upgrading to the latest version of Typo (along with a new design) so I’ll see if I can up that number :-)

  5. Fnor 9 days later:

    Thanks for your YSlow presentation. It’s a very interesting tool to improve websites speed.

    Regarding the number of HTTP connections due to lots of small images, I recommend reading : http://www.alistapart.com/articles/sprites/

  6. casual 9 days later:

    I’d have to agree with all of this except the comment about javascript. As a front end developer, it’s generally bad practice to have javascript anywhere but in the HEAD of a document.

  7. Tom 10 days later:

    One bit about ETags which I initially found quite confusing – YSlow is actually going to penalize you for using them, not for omitting them. The point they’re trying to make is that unless you’re using a cluster of servers, making use of ETags to detect file changes is unnecessary overhead.

    If you do use multiple servers though, the Apache directive to do this correctly is:

    FileETag MTime Size

    This prevents apache from using inode information in it’s etag calculation (which, for us rails users, is going to change on every run of capistrano).

    For more details, I found this blog post very helpful: http://phaedo.cx/archives/2007/07/25/tools-for-optimizing-your-website-etag-and-expire-headers-in-django-apache-and-lighttpd/

  8. Matt 5 months later:

    The expires header command doesn’t seem to be working (at least for me). And if I add the simpler expire: if (-f $request_filename) { expires 10y; break; }

    YSlow still complains that my static assets have no expires headers. Any thoughts?

Comments