Improving Java web site performance with asset caching 11

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:

  <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

Comments

Leave a comment

James about 9 hours later:

Very interesting article. This would be great to have on JavaLobby. If you’re interested, send me a mail and we can organise it.

Thanks James

arfio 2 days later:

pretty retarded

Brian 2 days later:

Isn’t 1 year or 10 years overkill? Most browsers will download it again the next day because the disk caches are set (by default) to relatively small sizes. Also, if your server shows the last modification time on these static files, a lot of browsers (especially FF) have algorithms to guess at how long they should be cached.

Jos Hirth 2 days later:

Yes, 10 years is overkill. For the most part 1 week will have the same effect. However, it doesn’t hurt if you’re going a bit overboard there.

Basically the only practical reason to use really far away expired headers is marking them as will-never-change for maintainers. Once they see the header they will understand what you did there. Whereas less excessive expiring dates can be less unambiguous.

SCdF 3 days later:

Shouldn’t your web server deal with that stuff for you?

Dmitry 3 days later:

there are several cache-related filters in JSOS: http://www.servletsuite.com/filters.htm

Chicago 19 days later:

A good man would prefer to be defeated than to defeat injustice by evil means.

Philadelphia 19 days later:

Eat a third and drink a third and leave the remaining third of your stomach empty. Then, when you get angry, there will be sufficient room for your rage.

Tiago Albineli Motta about 1 month later:

I think it’s better to put an apache between the client and the container.

Willie Wheeler 3 months later:

Funny, I wrote almost exactly the same filter about a week ago, for exactly the same reason (bad YSlow grade)… :-)

Pwhndvve 3 months later:

Rimsky went legate left buy cytotec dead hand estivities.

Comments

Spinner



You are viewing a mobilized version of this site...
View original page here

Mobilized by Mowser Mowser