Wicket Interface Speed-Up: Modifying Expires and Cache-Control Headers
August 15th, 2008 by Stefan Fußenegger | Published in Wicket | 11 Comments
This entry is part of my “Wicket Interface Speed-Up” series.
Let’s start with a short introduction before we talk about setting Expires and Cache-Control HTTP-Headers:
By default, all WebResources are cacheable for one hour, which is defined by WebResource.getCacheDuration():
protected int getCacheDuration() { return 3600; }
While one hour is nice, it would be beneficial to increase the value for resources that are known to change rarely (or never). In order to change that default to something different, one has to override this method. However, it’s not very obvious how to do that if you are used to this:
public MyPanel(String id) { super(id); add(HeaderContributor.forCss(MyPanel.class, "MyPanel.css")); // snip }
So how to cache MyPanel.css for more than an hour? In order to answer that question it’s nice to know what happens behind the scenes. HeaderContributor.forCss(…) returns a HeaderContributor that will do the following, as soon as the head section of your HTML is rendered:
public void renderHead(IHeaderResponse response) { response.renderCSSReference( new CompressedResourceReference(scope, path)); }
Okay, so we are dealing with a CompressedResourceReference. But how do ResourceReferences translate into the actual Resource? ResourceReferences will be translated into a URL using a SharedResourceRequestTarget and IRequestCodingStrategy.encode(…) (WebRequestCodingStrategy by default). The default implementation is as follows:
- to check if the IRequestTarget is mounted
- use ResourceReference.getSharedResourceKey() to encode a URL (those URL’s will start with “/resource”)
Every time ResourceReference.getSharedResourceKey() is called, Wicket will make sure that the resource is bound to the Application (i.e. stored in Application.getSharedResources()) and return a unique key made up of scope, path, locale and style. If the ResourceReference hasn’t been bound to the Application before, it will make sure, that Application.getSharedResources() will contain a Resource (not a ResourceReference!) by calling ResourceReference.newResource(). In the case of a CSS file, newResource() will return a CompressedPackageResource … but only if it isn’t already bound to the Application – well, you probably can’t check often enough
Okay, so now we know how ResourceReferences translate into Resources: They will be looked up in an application’s shared resources and be created if not already present. However, this check doesn’t take the type of ResourceReference into account. This means that a CompressedResourceReference needn’t translate into a CompressedPackageResource. It can also translate into anything else that has been bound before.
Therefore, the trick is to bind a shared resource to the application before it is used the first time. This can easily be done inside an Application’s init():
public void init() { Class scope = MyPanel.class; String path = "MyPanel.css"; WebResource resource = new CompressedPackageResource(scope, path, null, null) { protected int getCacheDuration() { return 3600 * 24 * 7; } }; getSharedResources().add(scope, path, null, null, resource); }
Therefore, you don’t have to change any components to change the cache interval of your resources. However, it would be desirable to make the current default of one hour configurable using Application’s settings. Furthermore, it would also be nice to get control over the newResource() method, e.g. using something like Application.getResourceFactory().newResource(ResourceReference ref).
Attention: An overflow in current Wicket versions (1.3.4 and 1.4-M3 respectively) leads to wrong values for the Expires header. Cache durations greater than Integer.MAX_VALUE/1000 seconds (which is approx. 25 days) cause wrong expiry dates. (see Wicket-1777). Bugfix will be available in next versions.
UPDATE: There is an ongoing discussion on my “Wicket Interface Speed-Up” on Wicket’s mailing list.
August 18th, 2008 at 8:49 pm (#)
HiInteresting read, thanks !
Just a question : you point some possible improvements of Wicket : do you plan to act upon them (like creating feature request or update the wiki wish list) ?Thanks in advanceJoseph
August 19th, 2008 at 8:15 am (#)
Hi Joseph,yes, I plan to do so. However, I want to finish my series first (planned this week) before I’ll go and submit a bunch – yes, some more to come
– of suggestions.Regards, Stefan
August 19th, 2008 at 10:05 am (#)
So for every resource , you need to change the code inside Application.init() ?That doesn’t sound very nice. Typically I would like to code the Application class once, and be done with it.
August 19th, 2008 at 11:14 am (#)
Unfortunately, yes you have to do it for every resource. That's why I propose to make the global default configurable or to add a resource factory, that can be replaced with a customized implementation.Currently, I am doing it this way:mountResources(CssScope.class, "/css/", "all.css", "user.css", "ie7.css");private ResourceReference[] mountResources(Class<?> scope, String mountPrefix, String… files) { // snip}In this method, I check the suffix of the file (".js", ".css", ".html", ".xml") and use an appropriate resource (one that minifys JS, removes whitespace from html, css, xml, …)I'll give more detail in my next posts.Regards, Stefan
August 19th, 2008 at 5:34 pm (#)
This is why I never liked how Wicket serves packaged resources from a jar that goes through my application server. I’d rather have my web server do this, then I don’t need to worry about this junk on the Java side.
August 19th, 2008 at 9:19 pm (#)
This is exactly why you cache it with an up front proxy such as nginx or varnish.In about a week I am also going to write a fairly extensive post with code samples on how to scale wicket. On what we did at fabulously40 to scale wicket. stateful web apps can easily be scaled with a little bit of creativity.
August 20th, 2008 at 8:02 am (#)
Thanks for you comments.Of course, resources could either be served by a different server (probably not a servlet container) or cached by up front proxies. However, speeding wicket interface rendering isn’t necessarily related do scaling it. Even small apps with a few hits where performance isn’t an issue benefit of resource caching. In such cases, additional servers or proxies are just an additional pain. I also like the idea of configuring resource serving where I need them: directly in my app, not in an Apache or proxy config file.best regards, Stefan
August 20th, 2008 at 11:54 am (#)
what i can do is make it a setting: IResourceSettings.getResourceCacheDuration()so people can change the default duration
August 20th, 2008 at 12:33 pm (#)
Hi Johan,Sounds good to me. However, I wanted to finish my series (one post yet to come – have you seen the second one?) and finally submit a list of possible improvements for discussion (end of this week probably).Stefan
November 30th, 2009 at 5:13 am (#)
Hello,
First of all, thank you for this!
Second: it may sounds like a basic Wicket question, but I can’t have this working and it was impossible to find documentation about shared resources.
Basically, I just want to load JQuery with the cache duration, so I added it to shared resources as you said, but I don’t know how to load it from the page, I tried:
add (JavascriptPackageResource.getHeaderContribution(“js/jquery.min.js”));
But the cache duration is not applied…
Thank you for your time!
Oskar
December 2nd, 2009 at 9:14 am (#)
Hi Oskar,
please send a quick-start (i.e. a small wicket application) that demonstrates your problem. I’ll have a look at it.
Cheers, Stefan