How to really speed up Web serving (and Plone) for your iPhone readers
Web servers (especially Plone, by default) work wonders in combination with an HTTP accelerator such as Varnish or Squid. But your iPhone readers are out of luck because of a grave bug on MobileSafari -- Plone sites are especially slow like molasses on the iPhone. Don't worry, here's a trick that will solve it.
The other day, just for kicks, I attempted to browse this very site with my iPhone (via WiFi). For those of you who don't know it yet, there is a third of a second in latency between my phone and the server serving this page.
Disaster. Not at all like the desktop experience.
How to get browsers and proxies to cache your content
Desktop browsers load each page element just once, and then they hold them in their browser cache. This is thanks to the very clever caching control settings for the page elements that basically tell your browser to keep them in the cache. All this trick necessitates is a single HTTP header that says:
Cache-Control: max-age=86400, s-maxage=86400, public, must-revalidate, proxy-revalidate
This snippet of text in the response header sent along with CSS stylesheets, JavaScript files, and icons on your site basically (in that order):
- tells your browser that the content is going to be fresh for one day (86.400 seconds)
- tells your cache (in my case, Varnish) the very same thing
- tells everyone that the content is public for everybody to see, so it's safe to save it
- tells your browser that, once the day is over, it must check (revalidate) to see if the response is still fresh; contrary to common sense, the standard requires revalidation only after the maximum age has passed, not for each request
- tells the proxy the same thing it told your browser about revalidation
to fetch the objects on your page, they go out in a flash directly from the proxy cache, and pipelined, to boot. As if this wasn't enough, intermediate proxies such as your ISPs will cache the content until it "expires", making those accesses blindingly fast for people using the same ISP. The negative effects of latency are thus minimized greatly.
Now, normally, desktop browsers would cache content anyway, despite the absence of this header. But keep in mind that the iPhone won't. So this HTTP response header is really important.
Why this header does not sit so well with the iPhone
Well, for two reasons:
- for starters the iPhone does not open four HTTP connections to your accelerator,
- It does not obey the orders Plone sent along with the objects!
In practice, the first time a visitor goes to your site, the iPhone loads your page, and then it loads all of the stylesheets, JavaScript scripts and icons, which on Plone sites takes an average of 25 requests multiplied by the latency between the phone and your site (greatly exacerbated if the user is on EDGE).
In my case, it was like fifteen seconds. For every tap of my finger. Madness!
So I did some experiments, which is usually what I tend to do when I discover something is not right. And I did find out an interesting thing: remember when we were discussing the "must revalidate" thingie? It turns out that the iPhone does the "common sense" thing instead of the "standard" thing. In other words, if the header says "must revalidate" (or, even more curiously, "proxy revalidate") the iPhone goes and asks the server if the content is fresh.
Twenty-five times.
Surprisingly, the phone was caching the problematic content. How did I know this? Easy: my accelerator was replying 304 Not Modified instead of 200 OK. -- the telltale sign that the phone is not re-downloading the files, just merely checking if they are fresh.
Twenty-five damned times.
The fix
Surprisingly, the fix is very simple: whenever an iPhone says "hey, I am an iPhone" in its user agent request header, all you have to do is strip out the revalidation orders in the cache control response headers. This is remarkably easy to do with proxies like Apache.
Not so with Varnish. Therefore, I will explain how to do it with Varnish. Those of you using NginX or Squid will have to map these concepts onto your expertise in these accelerators.
How to do it with Varnish
These are the relevant snippets of VCL that will get you there. All they do is, when the request is about to be hashed, if the user agent is the iPhone, then the object is "specialized" for the iPhone (meaning: cached separately), and later on, when the object is delivered to the phone, the cache control headers are customized for the iPhone as well, removing revalidation pragmas.
sub munge_iphone { if (req.http.User-Agent ~ "iPhone") { set req.hash += "iPhone"; set req.http.For-iPhone = "iPhone"; } } sub customize_obj_for_iphone { if (req.http.For-iPhone) { set obj.http.For-iPhone = req.http.For-iPhone; set obj.http.Cache-Control = regsub(obj.http.Cache-Control,", must-revalidate",""); set obj.http.Cache-Control = regsub(obj.http.Cache-Control,", proxy-revalidate",""); } } sub vcl_hash { call munge_iphone; } sub vcl_fetch { call customize_obj_for_iphone; }
And this is how you can go from 25 seconds' loading time to 1 second loading time.
Well, why lie? There is a stage that the iPhone still finds difficult, and that is the execution of the JavaScript built into the portal -- despite absolutely no network accesses, MobileSafari does freeze for a couple of seconds after most of the page has already been loaded. And sometimes the iPhone re-requests one of the JavaScript files (which are admittedly too big for its 50 KB per-file cache). But ideally, you should be able to get the iPhone to cache everything on your site, so the only cost is the HTML on each page.
Still, even with that freeze and the occasional reload, the difference in response times is astonishing. And as for the JavaScript freeze, you may want to trim the JavaScript down on your site a bit for iPhone users.
But that's out of the scope of this tutorial. See you around!
I appreciate the help of the good gents in the #plone channel on the FreeNode network.