Improving Java web site performance with asset caching 9

Posted by Matt Parrish Thu, 01 May 2008 23:04:00 GMT

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:

  
    StaticFileFilter
    org.pearware.web.filter.StaticFileFilter
  

  
    StaticFileFilter
    *.png
  

  
    StaticFileFilter
    *.jpg
  

  
    StaticFileFilter
    *.gif
  

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]

[/#macro]

[#macro script src]

[/#macro]

[#macro img src params…]
[#compress]

[/#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