I’ve been looking at performance issues for the site we’re building at work. The database is obviously going to be the biggest bottleneck, so we’ve configured 2nd-level caching for read-only tables and some read-mostly tables. We’ve also configured query caching for obvious candidates.
Currently the big hitter is the search page, and some profiling showed that we just needed a few extra indexes and some extra 2nd-level caching, but otherwise it performs pretty well. I still need to test it on production hardware with a large database – the stuff I’ve done so far is on my own high-end developer box with the database, server, and clients all running on the same box.
So I’m now working on applying the 14 rules from Steve Souders and the Yahoo performance team. YSlow has been very useful to measure improvement after each change, and of course Firebug is indispensable.
This is a Grails app, and Grails makes several parts of the process a lot easier, but of course all of these approaches apply to non-Grails apps too.
We also want to configure caching by using far-future Expires header. The best approach for this is to add an Expires header for the year 2038, 10 years from now, etc. and embed a version number in the url but serve the current file. For example instead of:
The client will get the latest contents of bar.js and the Expires header will tell the browser to cache forever. The version helps us when we deploy a new build and want to ensure that clients re-request the file to get the new latest version. After deploying the new version we’d want to render the new version:
We’ll still send them the contents of bar.js but it’ll be newer and since the name is different, the old version will be ignored (and removed from the client’s cache once it’s full) and they’ll use the latest code.
<link rel='stylesheet' type='text/css' href='/context/css/styles.css' />
<img src='/context/images/spacer.png' border='0' />
where ‘context’ is the context the app is deployed under (blank for the root context), and ‘ns’ is whatever namespace the tag is registered under. Each tag allows extra attributes and these are just passed through to the HTML (e.g. to specify the id, style, etc.)
I write out the version into a properties file for use by the custom tags at runtime so they can append the version into the rendered <script>, <link>, and <img> tag urls so they match up with the names in the war.
Embedded image urls in CSS files are a problem using this versioning scheme, but since I’m processing each file during the build, I take that opportunity to rewrite the urls inside all CSS files with the version embedded, so they also agree with the names in the war, e.g.
And I’m also putting together sprites where practical (stretched single-pixel background images and animated images aren’t candidates) to reduce the number of image requests. csssprites.com makes bundling together individual images easy and generates a table showing the position offsets for each image.
Using these techniques I’m seeing YSlow scores of 93-99 on all of our pages when deployed in production mode in Tomcat. We’re planning on using a CDN eventually, but for now I’ve cheated and added ‘localhost’ and ‘maps.google.com’ to the YSlow ‘extensions.firebug.yslow.cdnHostnames’ property since the YSlow score is significantly affected when not using a CDN that it knows about.
Of course developing locally we don’t use war files, so all of this must be disabled unless we’re in production mode. Grails makes this easy (using
GrailsUtil.environment), so the custom tags skip the step of appending the version in urls, and we’re able to serve uncompressed, unminified files from the file system with no Expires headers.