Static File Optimizations

August 11, 2010

GravatarBy Michael Snoyman

Things are coming into place very nicely for the Yesod 0.5 release. I'd originally thought this release would basically just be to match the release of persistent 0.2; instead, there are some major features making their way in. I've already mentioned the changes in Hamlet; here I want to address some issues revolving around static files.

sendfile system call

Probably the most powerful optimization available for serving static files is the sendfile system call. The WAI has support for this feature, and Yesod inherits that. So if you want to send a file, you should always use sendFile.

One other thing: a number of web servers provide access to this optimization for (Fast)CGI scripts via an HTTP header. wai-handler-fastcgi 0.2.1 added support for this feature. And on a complete tangent, I'm testing wai-handler-fastcgi 0.2.2 which introduces multi-threaded support, so you don't need to run your app as multiple processes.

Separate domain

Another technique to speed up static files is serving from a cookie-less domain. The Yesod approach to this is to still create the route for serving static content with the static subsite, but then use the urlRenderOverride function to point this at a separate domain name in your production server.

The yesod scaffolding tool for Yesod 0.5 will generate a site template that does all of this for you automatically, so you don't need to guess at how to approach it. If you want to see it in action, there's an example on github.

Another advantage of this approach is that we can bypass the Yesod application entirely for serving these static files.

Type-safety anybody?

Let's take a little tangent- we'll get back to optimizations in a second, trust me. Have you ever been annoyed that you're hardcoding these strings in your source code? You know, path names: what if I mistyped it, what if I move the file, and so on. Yesod's got your back here too: staticFiles is a little bit of template haskell code that gets a list of all of your static files at compile time and creates identifiers for all of them.

Hashes

And here comes the awesome new feature that's holding up the Yesod 0.5 release: hashes. When the staticFiles function mentioned above gets the list of files in your static folder, it also gets a hash value for each file. When you render that identifier to a URL, it includes that hash value in the query string of the URL.

So here's a corny line: what rhymes with hashing? Caching. You can now set your expiration headers into the distant future with impunity. If the file changes, the hash will change, and therefore the URL will change, and therefore the browser will download a new version. There is no runtime penalty for this, since the hashes are all calculated at compile time.

And if you follow my advice and serve your static files from a separate domain name, you can simply have your server set an expiration header in the far future for that domain. In nginx, for example, I'm now able to use "header: max;".

Cassius and Julius

I had a little poll recently to determine the names for the CSS and Javascript templating languages to be including with Hamlet, and the winners were Cassius and Julius. Due to the cassiusFileDebug and juliusFileDebug functions, you no longer need to recompile your application to see changes to styles and javascript added to widgets. By using these template languages, you get to have variable interpolation and type-safe URLs in your CSS and JS as well. Seems like a great feature.

The downside is that styles and javascript in Yesod 0.4 get embedded in the head tag, when what we'd really like to do is have them stored separately so we can get the benefits of caching. In fact, if we could do some hashing as well, that would be amazing, right?

Yesod 0.5 is introducing the function addStaticContent; by default, the function does nothing. However, it can also be used to store contents of a file somewhere and return a URL from which the contents can be accessed. Yesod uses this function internally to reference CSS and JS code from a URL instead of embedding it.

The default site template will simply store the contents in a filename based on the hash of the contents; once again, if you follow the advice given above, these files will be served on a separate domain name without ever touching your Yesod application, with an expiration date far in the future, reaping maximum performance.

However, for sites that want to get even better performance, the sky's the limit. For example, you could write an addStaticContent function that stores the contents in a memcached database, or distributes the contents to a CDN.

Conclusion

I still think the major feature of the next release will be database migrations in persistent. However, the debug mode for Hamlet is also very compellling, as are these modifications to Yesod. The nice thing about this release is that it's introducing very few breaking changes, so it should be an easy migration from Yesod 0.4.

I was on a train for a long time this week, so I finally started a project I've been itching to do for a while: a Yesod book. It's going to be served on the Yesod documentation website, but will be going into much more depth than I do elsewhere. I assume my blog readers have ADD (no offense), and that people on the main docs site just want a quick example of how to get something done. The book, on the other hand, will try to approach both the why and the how.

Since I was on the train when writing, there are still a lot of typos, and it's very incomplete, but I'm hoping to do a bit of writing every day. Stay tuned.

Comments

comments powered by Disqus

Archives