Wicket Interface Speed-Up: Caching Resources Forever
August 20th, 2008 by Stefan Fußenegger | Published in Wicket | 8 Comments
This entry is part of my “Wicket Interface Speed-Up” series.
In my last post, I explained how to change (i.e. increase) the time a client should cache resources like JavaScript and CSS files served by a Wicket application. In this post, I’ll show you how to cache resources for a year. Caching for a year is close enough to forever and a recommendation by W3C:
“To mark a response as “never expires,” an origin server sends an Expires date approximately one year from the time the response is sent. HTTP/1.1 servers SHOULD NOT send Expires dates more than one year in the future.”
Allowing users to cache resources forever is an efficient way to reduce the number of requests, server load and interface loading time (with a “primed cache”). However, aggressive caching comes a long with a major problem: cached versions of resources might be outdated but still valid for a long time. YSlow’s suggestion:
“Keep in mind, if you use a far future Expires header you have to change the component’s filename whenever the component changes. At Yahoo! we often make this step part of the build process: a version number is embedded in the component’s filename, for example, yahoo_2.0.6.js.”
While I don’t like the idea to do such stuff at build time, I do back up the idea of using version numbers. I prefer determining the version number of resources at application startup (i.e. before mounting them).
This leads to another question though: How to get a version for each resource? Using your application’s version (if you have one) might be appropriate but depends on how often you deploy new versions, as a new version will make all cached (i.e. even unchanged) resources obsolete.
As we are deploying new builds quite regularly, I am using SVN revision numbers as resource versions (e.g. all-1234.css instead of all.css). If you now reckon that the revision number isn’t available at startup, you’re certainly right … well, normally it isn’t. In order to retrieve the revision number at runtime, I introduced a simple header to all my JS and CSS files. This header (a single line) looks like this:
/* $Revision$ */
After adding this header to your files, enable the SVN keyword “Revision” (see “svn:keywords” section). After commiting, SVN will expand the above line to
/* $Revision: 1234 $ */
This line can now easily be parsed with a simple regular expression at startup;
Note: You can also configure SVN to automatically enable SVN keywords (see “Automatic Property Setting” seciton).
Now that we have a version for our resources, the Application init code looks like this:
protected void init() { // snip mountVersionedResource("/css/", CssScope.class, "all.css"); } protected ResourceReference mountVersionedResource(String prefix, Class scope, String path) { String unversionedPath = prefix + path; String versionedPath = prefix + getVersionedPath(scope, path); ResourceReference ref = getResourceRef(scope, path); mountSharedResource(versionedPath, ref.getSharedResourceKey()); // redirect all.css to all-1234.css, just to be safe if (!unversionedPath.equals(versionedPath)) { mount(new RedirectStrategy(unversionedPath, WebPage.class, null, versionedPath)); } return ref; } private final class RedirectStrategy extends BookmarkablePageRequestTargetUrlCodingStrategy { private final String _redirectPath; private RedirectStrategy(String mountPath, Class pageClass, String pageMapName, String redirectPath) { super(mountPath, pageClass, pageMapName); _redirectPath = redirectPath; } public IRequestTarget decode(final RequestParameters params) { return new RedirectRequestTarget(_redirectPath); } String getVersionedPath(Class scope, String path){ // snip: removed for simplicity, i'll provide code if desired } protected ResourceReference getResourceRef(Class scope,String path) { ResourceReference ref; if (path.endsWith(".js")) { ref = new ResourceReference(scope, path) { protected Resource newResource() { return new JavascriptPackageResource(scope,path,null,null) { protected int getCacheDuration() { return CACHE_DURATION; } }; } }; } else if (path.endsWith(".css")) { // snip: choose appropriate resource references } ref.bind(this); return ref; } }
Yay, now we have versioned resources that can be cached forever (well, still only for about 20 days, see WICKET-1777). And yes, you still use the resources as usual:
public MyPanel(String id) { super(id); add(HeaderContributor.forCss(CssScope.class, "all.css")); // snip }
Resulting in
UPDATE: There is an ongoing discussion on my “Wicket Interface Speed-Up” on Wicket’s mailing list.
August 20th, 2008 at 3:19 pm (#)
Not sure if you know this but, you can also setup your resources with a GET parameter which will force the browser to re-download the file, for example:/css/foo.css?v=0.1 … foo.css?v=0.2
August 20th, 2008 at 3:31 pm (#)
in deed, using parameters is just another way of creating a unique URL for a particular resource version. However, I’m not aware of a Wicket-way to do this. How would you do this? It could probably be done by changing getVersionedPath(..) in my implementation to return a different string, but that’s probably not what you meant.
August 25th, 2008 at 9:18 am (#)
Hi StefanI just write something about caching on my blog too, it goes well in hand with what you’ve written:http://ninomartinez.wordpress.com/2008/08/25/pump-your-java-with-caching/
August 27th, 2008 at 10:22 am (#)
According to Steve Souders, putting the version into the filename has certain advantages.In that regards: Would you mind posting your getVersionedPath implementation?ThanksJörn
August 27th, 2008 at 10:49 am (#)
Hi Nino, Jörn,Thanks for your comments and links.@Nino:Server-side and client-side go well in hand in deed. However, one should note that these are completely different topics.@Jörn;Here is my implementation: molindo.at/files/Version.java. It isn’t a valid Java class though, just some code snippets pasted together. Anyway, I hope it helps!Best regards, Stefan
August 27th, 2008 at 11:30 am (#)
Thanks Stefan, that helps a lot!Yet missing are the methods getResourcePath and getResourceRef and the class CssScope – I have no idea what that does.
April 2nd, 2009 at 8:47 am (#)
Thank you. That helped a lot.
After reading your post I came to this part of code:
protected void mountVersionedResource(String prefix, Class scope, String path)
{
String versionedPath = prefix + getVersionedPath( path, System.currentTimeMillis() );
ResourceReference ref = new ResourceReference(scope,path);
mountSharedResource(versionedPath, ref.getSharedResourceKey());
WebResource resource = new CompressedPackageResource(scope, versionedPath, null, null)
{
@Override
protected int getCacheDuration()
{
return 3600 * 24 * 7; // one week
}
};
getSharedResources().add(scope, path, null, null, resource);
}
protected String getVersionedPath(final String filePath, final long version)
{
return Strings.beforeLast(filePath, ‘.’) + “-” + version + “.” + Strings.afterLast(filePath, ‘.’);
}
July 12th, 2011 at 3:04 am (#)
Hi Stefan,
Thanks for your blog post. It was very helpful. I’m looking at doing something similar, exception I’m going to name the file names in the format of _sha256sum.. However, I would like to make this the default behavior for all requested components. I’ve overridden the ResourceLoader to calculate the sums for each resource as it’s loaded. However I can’t seem to find a global hook into resource creation short of an instantiation listener. Is there any other way to create the mapping for the resource without requiring the developer to call the custom code?
Thanks,
Todd