Welcome to End Point’s blog

Ongoing observations by End Point people

Speeding up the Spree demo site

There's a lot that can be done to speed up Spree, and Rails apps in general. Here I'm not going to deal with most of that. Instead I want to show how easy it is to speed up page delivery using standard HTTP server tuning techniques, demonstrated on

First, let's get a baseline performance measure from the excellent service using their remote Internet Explorer 7 tests:

  • First page load time: 2.1 seconds
  • Repeat page load time: 1.5 seconds

The repeat load is faster because the browser has images, JavaScript, and CSS cached, but it still has to check back with the server to make sure they haven't changed. Full details are in this initial report.

The site is run on a Xen VPS with 512 MB RAM, CentOS 5 i386, Apache 2.2, and Passenger 2.2. There were several things to tune in the Apache httpd.conf configuration:

  • mod_deflate was already enabled. Good. That's a big help.
  • Enable HTTP keepalive: KeepAlive On and KeepAliveTimeout 3
  • Limit Apache children to keep RAM available for Rails: StartServers 5, MinSpareServers 2, MaxSpareServers 5
  • Limit Passenger pool size to 2 child processes (down from the default 6), to queue extra requests instead of using slow swap memory: PassengerMaxPoolSize 2
  • Enable browser & intermediate proxy caching of static files: ExpiresActive On and ExpiresByType image/jpeg "access plus 2 hours" etc. (see below for full example)
  • Disable ETags which aren't necessary once Expires is enabled: FileETag None and Header unset ETag
  • Disable unused Apache modules: free up memory by commenting out LoadModule proxy, proxy_http, info, logio, usertrack, speling, userdir, negotiation, vhost_alias, dav_fs, autoindex, most authn_* and authz_* modules
  • Disable SSLv2 (for security and PCI compliance, not performance): SSLProtocol all -SSLv2 and SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:-LOW:-SSLv2:-EXP

After making these changes, without tuning Rails, Spree, or the database at all, a new run reports:

  • First page load time: 1.2 seconds
  • Repeat page load time: 0.4 seconds

That's an easy improvement, a reduction of 0.9 seconds for the initial load and 1.1 seconds for a repeat load! Complete details are in this follow-on report.

The biggest wins came from enabling HTTP keepalive, which allows serving multiple files from a single HTTP connection, and enabling static file caching which eliminates the majority of requests once the images, JavaScript, and CSS are cached in the browser.

Note that many of the resource-limiting changes I made above to Apache and Passenger would be too restrictive if more RAM or CPU were available, as is typical on a dedicated server with 2 GB RAM or more. But when running on a memory-constrained VPS, it's important to put such limits in place or you'll practically undo any other tuning efforts you make.

I wrote about these topics a year ago in a blog post about Interchange ecommerce performance optimization. I've since expanded the list of MIME types I typically enable static asset caching for in Apache. Here's a sample configuration snippet to put in the <VirtualHost> container in httpd.conf:

    ExpiresActive On
    ExpiresByType image/gif   "access plus 2 hours"
    ExpiresByType image/jpeg  "access plus 2 hours"
    ExpiresByType image/png   "access plus 2 hours"
    ExpiresByType image/tiff  "access plus 2 hours"
    ExpiresByType text/css    "access plus 2 hours"
    ExpiresByType image/bmp   "access plus 2 hours"
    ExpiresByType video/x-flv "access plus 2 hours"
    ExpiresByType video/mpeg  "access plus 2 hours"
    ExpiresByType video/quicktime "access plus 2 hours"
    ExpiresByType video/x-ms-asf  "access plus 2 hours"
    ExpiresByType video/x-ms-wm   "access plus 2 hours"
    ExpiresByType video/x-ms-wmv  "access plus 2 hours"
    ExpiresByType video/x-ms-wmx  "access plus 2 hours"
    ExpiresByType video/x-ms-wvx  "access plus 2 hours"
    ExpiresByType video/x-msvideo "access plus 2 hours"
    ExpiresByType application/postscript        "access plus 2 hours"
    ExpiresByType application/msword            "access plus 2 hours"
    ExpiresByType application/x-javascript      "access plus 2 hours"
    ExpiresByType application/x-shockwave-flash "access plus 2 hours"
    ExpiresByType image/      "access plus 2 hours"
    ExpiresByType application/ "access plus 2 hours"
    ExpiresByType text/x-component              "access plus 2 hours"

Of course you'll still need to tune your Spree application and database, but why not tune the web server to get the best performance you can there?


David Christensen said...

I'm distilling the essence of our IRC conversation here:

This fairly large list of specific ExpiresByType is used due to a couple reasons:

You can set a default Expires policy for all MIME types, however this is overly broad in cases where a URI is session based; for instance, dynamically-generated PDFs, images, or JSON. Additionally, some resources may be undesireable to cache, such as captcha images, time-based PDF or Zip files, etc.

You can specify nothing and have your app provide its own, but that won't cover static files Apache serves directly.

If you can guarantee everything gets a unique URI, it's safe to set a default caching policy. But in practice most apps have some reused URIs somewhere or other, that are session based. So apps that follow RESTful principles would be okay, but reusing URIs with an overly long caching duration could end up reusing stale files for what are contextually different resources.

Ideally you set up a default caching level while developing the app, and make sure it's all sane, however often you have to come in after the fact and tread carefully around sessions, captchas, etc.

Many/most of the MIME types showed are never used, like the video/x-ms* ones. So the list is a little more extensive than most people need. It is easier to just utilize this same list to avoid re-figuring such things out later.

Jon Jensen said...

One other thing to note is that most web performance analysis tools will complain about a caching lifetime less than a week or even longer.

Theoretically it should be fine to set a cache expiration time a week, month or even year out in the future. You can always change the URI of anything you want to update sooner than that, right?

But what happens if you accidentally mess up an important URIs? Consider the serious harm of a badly mangled resource at / or /favicon.ico or /robots.txt or a published URL you can't change.

You also don't likely want RSS feeds or pages with frequently-updated news to be cached the same duration as static images.

In the end, I feel like a few hours is a nice balance between performance and bandwidth savings on the one hand, and the safety of being able to update the resource at a given URI in a timely manner. Knowing that browsers will re-fetch a botched file by the morning at the latest is a pretty good bit of disaster insurance.

Steph Skardal said...

Great example, Jon. It's great to know there are plenty of options (and see the numbers) for performance optimization before you get into Rails and/or database optimization in Spree.

Jon Jensen said...

An interesting new option to accomplish these same kinds of optimizations and more is Google's new Apache module mod_pagespeed.

It's kind of scary how interventive it can be with HTML, CSS, and JavaScript output, but it's fully configurable.