Improving Java web site performance with asset caching
In this post, I’ll be talking about a solution I developed at my day job to improve the performance of our web site by allowing the browser to cache JavaScript, CSS, and image files. We were noticing that much of our traffic was from requests for these assets, rather than our page content. Since these asset files rarely change (once per production deployment), we wanted to have the user’s browser cache them until the next build.
We use Yahoo’s YSlow Firefox plugin to analyze the performance characteristics of our site and we were getting bad grades for the following category: Add an Expires or a Cache-Control Header. Images are easy enough to cache. Just add an HTTP response header for the images to have them expire 10 years in the future. If you ever need to change an image, instead of modifying it, just create a new one with a different URL. To set this exipres header, I created the following class, called StaticFileFilter:
package org.pearware.web.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author Matt Parrish
*/
public class StaticFileFilter implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
long expires = 365*24*60*60*1000;
httpResponse.setDateHeader("Expires", System.currentTimeMillis() + expires);
httpResponse.setHeader("Cache-Control", "max-age=" + expires);
}
public void init(FilterConfig config) throws ServletException {
}
}In web.xml, we add this filter for all images:
<filter>
<filter-name>StaticFileFilter</filter-name>
<filter-class>org.pearware.web.filter.StaticFileFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>StaticFileFilter</filter-name>
<url-pattern>*.png</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>StaticFileFilter</filter-name>
<url-pattern>*.jpg</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>StaticFileFilter</filter-name>
<url-pattern>*.gif</url-pattern>
</filter-mapping>So, now we have our image files cached for 10 years. JavaScript and CSS files, however, tend to change frequently, possibly with every new build. Unlike with images, it really becomes a pain to rename the files, especially when using Source Control and you want to track the changes of the files over time. My solution was to have the build number become part of the URL for these assets, without having to change the names or locations of the files. The StaticFileFilter then becomes responsible for translating the build-dependent URL into the location for the actual resource. Here’s the new body of the StaticFileFilter.
String buildNumber = SomeWayTo.getBuildNumber();
String oldRequestURI = httpRequest.getRequestURI();
String requestURI = oldRequestURI.replaceFirst(buildNumber + "/(js|css|images)", "$1");
long expires = 365*24*60*60*1000;
httpResponse.setDateHeader("Expires", System.currentTimeMillis() + expires);
httpResponse.setHeader("Cache-Control", "max-age=" + expires);
request.getRequestDispatcher(requestURI).forward(request, response);What this does is change the requested URI from, say, /2.3/images/sample.jpg to /images/sample.jpg. The last line of code is responsible for requesting the image at the actual path. The only magic here is how to get the build number. For us, we update a properties file with the build number before every QA build and have a class that reads in the property.
The other piece to this is that our XHTML page needs to reference images as <img src=”/2.3/images/sample.jpg”/> instead of <img src=”/images/sample.jpg”/>. Since the build number changes with each deployment, we need to dynamically generate this build number. Here’s how I’m doing it, using Freemarker:
[#macro css href]
<link rel="stylesheet" type="text/css" href="${base}/${someWayToGet.buildNumber}/${href}.css"/>
[/#macro]
[#macro script src]
<script type="text/javascript" src="${base}/${someWayToGet.buildNumber}/${src}.js"></script>
[/#macro]
[#macro img src params...]
[#compress]
<img src="${base}/${someWayToGet.buildNumber}/${src}"[#list params?keys as attr] ${attr}="${params[attr]}"[/#list]/>
[/#compress]
[/#macro]
[@script src="js/prototype"/]
[@css href="css/main"/]
[@img src="images/logo.png" alt="Logo"/]What you can see is that the StaticFileFilter is setup to serve all of our CSS, JavaScript and images. The benefit is that we are now free to modify any of our assets, yet still instruct the browser to cache them for 10 years. When we push out a new build, the browser will see new URL’s for the assets due to the new build number. Our deployment doesn’t change; we don’t need to move files around to support this. It’s been working great for us. Our YSlow score has gone up and our HTTP traffic for these files has decreased dramatically, freeing up our server and network for serving up the actual content of our application
RadiantOnRails released on RubyForge
RadiantOnRails is a Radiant extension I created to allow a Rails application to co-exist with Radiant, giving the developer the best of both (dynamic and static) worlds. You can now visit the new project page on RubyForge.
This extension will be a major piece for the website I’m currently working on, RealIdaho.com. Most of the pages for the site just display static content about cities, but there are other portions of the site that will be fully-dynamic Rails pages. This extension allows me to combine both portions of the site into one application so we can develop the Rails pages for displaying the data-driven pages and still leverage the wonderful user interface created by the Radiant team. This will allow the realtor to make content changes without me having to make code changes and redeploy.
RadiantOnRails currently allows Radiant snippets to be inserted into Rails views and the next step is to allow the Rails views to use the Radiant layouts so that the look ‘n’ feel of the site is consistent, while keeping the views DRY. I’ll also be working with Loren Johnson to make Radiant available as a plugin which will make integrating Radiant with Rails even easier.
Switching from Pound to Nginx
I just switched some Ruby on Rails apps I’m running from Pound to Nginx based on the results from some articles I’ve read online. The two biggest advantages of Nginx are 1. It’s raw performance, and 2. It can serve up static files, which is great for running Capistrano’s disable_web command to show a maintenance page when redeploying an application.
At work, we’re working on a standard Ruby on Rails setup and are currently investigating two options. The first is a Mongrel cluster running behind Nginx, as I described above. The second option is fronting the Mongrel cluster with Lighttpd. Since the 1.4.x series of Lighttpd is known to have some issues with it’s mod_proxy implementation, we would use Pen until a stable 1.5 version is released.
I’ll post another article once we have finished our evaluation and chosen which option we’re going to deploy at work. Stay tuned…
Cool Google Maps flight simulator
Now this is a pretty cool idea!
Rails redeploy issue resolved
I just redeployed a rails site for a client of mine, Real Idaho, using Capistrano and it didn’t work properly. I was able to resolve the issue, and here are the details in case this ever happens to you…
Running
$ rake deploy
gave the following output (some content suppressed):
...
transaction: commit
* executing task restart
* executing "/home/*****/apps/*****/current/script/process/reaper -d 'dispatch.fcgi'"
servers: ["216.118.83.207"]
[216.118.83.207] executing command
** [out :: 216.118.83.207] bash: /home/*****/apps/*****/current/script/process/reaper: Permission denied
command finished
rake aborted!
command "/home/******/apps/******/current/script/process/reaper -d 'dispatch.fcgi'" failed on 216.***.***.***
The problem is that the files in script/ and the dispatch.* files in public need to be executable, but when Capistrano pulls down the latest subversion code, it defaults those files to the permissions:
-rw-r--r--
which is just read/write.
In order to have subversion pull down the files with the correct, executable, permissions, you must run the following command on each executable file and commit them to subversion:
$ svn propset svn:executable
I found the following article, Subversion Primer for Rails projects, which has a nice script to automate the task of setting the necessary files as executable. Here’s the script:
$ svn propset svn:executable "*" `find script -type f | grep -v '.svn'` public/dispatch.*
Then, run
$ svn commit
to commit those changes to Subversion.
I think this should resolve the issue, but I haven’t redeployed yet, so I haven’t verified that this will really fix my deployment issue.
Solar Wi-Fi To Bring Net to Developing Countries
I think this is a very cool idea and one I am very glad to see happening. I really think efforts like this can be worldchanging, and do a lot to help people, especially children, in the developing world to have more opportunities. The project is Green WiFi and is founded by Bruce Baikie, former manager of strategic marketing and solutions for the telecommunications industry at Sun Microsystems, and Marc Pomerleau, currently a member of Sun Microsystem’s identity management marketing team. According to their site:
Is your site really Web 2.0? 1
Now you can run your website through a handy-dandy Web 2.0 validator, to see if it really is Web 2.0. This site was put together by 30 Second Rule, a website design and development company in Phoenix that is active in the Ruby community. My blog’s score should be upped by writing this article! :)