A little while ago I ran YSlow, a Firefox plugin that evaluates how quickly a web page loads, against an Application Builder based app I’d written. YSlow gave my site a grade C, with an overall performance score of 71 (out of 100). Some of the suggestions that YSlow offered are not simple to implement — like using a Content Delivery Network. Others should be doable with a little effort, like combining some of the YUI scripts to reduce the number of HTTP requests. One of the suggestions stood out as something that ought be simple, but I didn’t know how: Add Expires headers.
Add Expires Headers
YSlow found 34 static components without a far-future expiration date. That led me to investigate how one would set that expiration date. In protocol terms, there is a header field called Expires (section 14.21). You give it a date in a particular format and browsers will know they don’t need to refresh that file. It turns out that MarkLogic Server provides a simple way to set this: the static expires setting on an HTTP application server’s config page. The value is set in seconds. The protocol specifies that the value should not be more than one year in the future. We can set this value in MarkLogic Server to one year by making it (365 day * 24 hours * 60 minutes * 60 seconds = ) 31,536,000 seconds.
To test, use Firebug‘s Net tab to look at the response headers. I’m seeing that by default, the Expires header on CSS files is one hour ahead of the request timestamp, which matches the 3600 value that is in static expires field by default. When we change it to 31536000, we get an Expires header one year out. That moves the YSlow score up to 80, earning a B. Much better!
Updating Static Content
So far so good, but we’ve introduced a new problem. Suppose we develop some new CSS or JavaScript. We’ll have browsers out there that won’t even ask for it for a year. That will definitely lead to a broken site for some users. What we want is for browsers to not bother asking for content, unless it changes. But how would we tell them that it has?
I’ll make use of Ryan Grimm’s properties module, which you can find among the source for RunDMC. This module lets us set up properties by way of using MarkLogic Server’s namespace feature (Admin interface -> Configure -> Groups -> Default -> Namespaces). We can easily change the value through the interface, and we can access it easily in code using the properties module. Here are the steps:
Set up a property
Set up a namespace with a prefix of “release” namespace URI of “http://xqdev.com/prop/string/-r1”. That gives us a property called “release” with a value of “-r1”.
Import the properties module to appfunctions
In /custom/appfunctions.xqy, import the prop module.
import module namespace prop = "http://xqdev.com/prop" at "/custom/modules/properties.xqy";
Get the property
Declare a variable ($RELEASE) and assign it the release property:
declare variable $RELEASE := prop:get("release");
Append the release suffix
For any static content that you might change (CSS, JavaScript, images), add the release property to the name of the reference. In appfunctions.xqy, the $ADDITIONAL-CSS variable includes /custom/appcss.css. Here’s how we change that line:
<link xmlns='http://www.w3.org/1999/xhtml' rel='stylesheet' type='text/css' href='/custom/appcss{$RELEASE}.css'/>
At this point, the browser won’t request /custom/appcss.css, it will request /custom/appcss-r1.css. That brings up one minor problem: there is no such file. That’s why we need our next steps.
Import the properties module to rewrite.xqy
As a first step, I should mention that if I’m going to change the rewrite module, I copy it into the /custom directory — don’t forget to change the appserver config to use it. Now, import the module, just like you did for appfuncions.
Get the property
We put the same “declare variable $RELEASE …” in rewrite.xqy that we put in appfunctions above.
Remove the release suffix
Here’s the trickery that makes it work. First, we tell the browser to request /custom/appcss-r1.css, then we rewrite the URL to point to the actual file.
let $new-url := fn:replace($new-url, $RELEASE, "")
This line goes near the end, just before we return the URL.
Time for a change
To test it out, I bring up Firebug’s Net tab and refresh the page. Among the HTTP requests is appcss-r1.css. We know that on the server side, that’s getting rewritten to appcss.css. Looking at the headers, the Expires date is a year out. With that set, the browser knows that it can cache that file, saving an HTTP request.
Sometime down the road, you make a change. Time to put that release property to work. When you need to tell the browser that there is a new version for it to retrieve, just go to Configure -> Groups -> Default -> Namespaces, and change the last part of the release URI from -r1 to -r2. Let’s look at what happens.
- With release set to “-r1”, a user hits your page and gets /custom/appcss-r1.css, with an Expires header a year out.
- As the user hits other pages, the browser pulls the appcss-r1.css file from its cache.
- You make a change to appcss.css and change the release to “-r2”.
- On the next request, the browser will make a request for /custom/appcss-r2.css. That’s not in the cache, so an HTTP request will be sent.
- When the server gets the request, it strips off the -r2 and hands back the newly modified /custom/appcss.css.
There you have it: how to set the Expires header, and how to tell the browser that there is a new version to retrieve. How much your YSlow score increases will depend on a lot of factors, but this will help.
Tags: css, marklogic, xquery, yslow