Most of Metail’s try-it-on tech is delivered as a single page JavaScript application that embeds inside retailer sites, calling back to Metail-hosted services in Europe over HTTPS/JSON. In this post, Nick Day describes some of the more difficult trade-offs that we needed to make to make the best use of Content Distribution Networks to improve the speed of our app.

tl;dr When you’re using a CDN to accelerate the delivery of a 3rd party HTTPS / AJAX app to users far away, you end up having to choose your devils. We chose the one called “pre-flight OPTIONS request”.

We’re currently working with Dafiti, a Brazilian company whose user base is almost exclusively in-country. Our web-application and web-cache servers are based in the UK, so to aid user-experience we’ve moved as much as possible of our static and dynamic content so it’s served through a CDN (CloudFront, which handily has edge nodes in São Paulo).  

The last piece of this “CDN-ifying” has been to serve the initial HTML page from CloudFront.  Naturally, this has a large impact on page load speeds as the browser can do nothing else until this resource has been obtained; so fulfilling the request from a nearby CloudFront node is hugely preferable to having it trot under the Atlantic to hit our servers and then back.  However, as this page is now being served from the CDN’s domain, browser security constraints mean that any AJAX requests being made from it must either:

  1. also go through the CDN domain, or
  2. be served directly from your service domain using cross-origin resource sharing (CORS)

For cacheable content obtained through AJAX (in our case things like garment information) the preferred option is clear; serve it through the CDN.  The decision is more involved for content that you don’t want to or can’t cache, like requests that you need to be up-to-date (e.g. account details) or update requests. In this case your choice is for the lesser of two evils.

Serving through the CDN:

  • you’re making every one of these requests take an indirect route, as the CDN will be passing them all on for your servers to handle
  • on top of that there’s an extra hit; SSL de/encryption must happen in the CDN for these requests to be forwarded
  • you have to manage potentially complex CDN config to do the right thing for each request.  You wouldn’t have to do this with your typical CDN-cached content, but in practise you may need to add cookies and headers to a whitelist for the CDN to forward.  CloudFront makes this problem extra-fun by lacking a programmatic way to update/version your distribution configuration, making it easy to take down your service accidentally, and hard to roll back.
  • you’re paying (real money) each time you route one of these requests (somewhat unnecessarily) through a CDN

Alternatively, if you send these requests directly to your service domain:

  • you’ll need to implement and maintain CORS handling for those endpoints being requested from other domains
  • for requests deemed “non-simple” by the CORS spec (methods other than GET/HEAD or POST; using Content-Type other than a very strict set, which surprisingly does not include JSON; or using custom headers) the browser will automatically make a pre-flight OPTIONS request to check that it is able to make the request before actually making the one you desire.  This is a big deal if your longer-than-you’d-like cross-Atlantic request suddenly becomes two!  It was this point that almost pushed me toward taking the CDN route for these requests.

Fortunately for us, there are two saving graces with this latter choice of sending the requests directly to our service domain:

  1. All of the requests in our app that require a pre-flight request are non-blocking, that is that they don’t require a user to wait before viewing part of, or accessing functionality in the app. They’re all “update in the background” kind of requests. Granted, if these take too long you could imagine timing issues creeping in, but we should be coding defensively to guard against those kind of asynchronous problems anyway.
  2. The CORS spec allows you to specify a caching max-age for the OPTIONS requests. Of course, since you’re not going through a CDN, this caching will be on a per-user basis against the web-cache. At the moment, unfortunately for us, even a cache hit must come to Europe. The answer here is that it’s probably going to make sense for us to move web-caches closer to where the users are, even if the apps stay rooted in the UK.  Another slightly niggling point is that some browsers currently have an enforced maximum cache period that’s quite short for pre-flight requests (e.g. Chrome is 5 minutes).  As this is shorter than our average user session, it means that it’s likely a user will have to make those same requests more than once per session.  From half a world away, that’s not ideal.

Taking all of these points into consideration, we favoured sending requests directly to our service domain; preferring to take the hit with the “background” requests that require a pre-flight in order to improve performance of most of our non-caching requests.