The web's same-origin policy is one of the cornerstone mechanisms upon which web security is built. It restricts the way that resources can be shared between web applications that have differing protocols, hosts or ports.

Mike Quinn [CC BY-SA 2.0], via Wikimedia

A few years ago, the Cross-Origin Resource Sharing (aka CORS) specification extended the policy to enable various scenarios that were previously restricted. While the new scenarios are certainly useful, they introduce new considerations for the performance minded. The remainder of this post will cover those considerations in full, and touch on a related issue with the Resource Timing specification as well.

CORS Performance

There are two types of CORS requests. Simple requests, which browsers and servers treat as they always have. "Complex" requests, on the other hand, require an additional "preflight check" to ensure that the server is aware of and prepared for requests from another origin. Preflight checks are accomplished using a separate HTTP OPTIONS request, along with several headers that describe to the server the client's origin, the HTTP method it would like to execute and the headers it will send. With the preflight description in hand, the server can choose to indicate if it would attempt to process a request like that or not. If the indication is positive, then the client issues the actual request.

The challenge with CORS comes from these preflight HTTP requests, which introduce additional round trips and reduce performance. That said, it's no surprise that eliminating as many preflight requests as possible is desired. After all, minimizing the number of HTTP requests has long stood as the number one best practice in web performance.

There are two strategies for reducing preflight requests:

1. Design your API and/or serve your content so that is only uses these HTTP methods and headers:

  • HEAD
  • GET
  • POST
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (but only with a value of application/x-www-form-urlencoded, multipart/form-data or text/plain)

To retain backwards compatibility, cross-origin requests that meet this criteria do not require preflight requests. These restrictions are quite limiting, however, a little cleverness with the resource's URI can open up all sort of possibilities. For example, your application could provide semantics to override the HTTP method via the query string (e.g. http://example.com?method_override=DELETE). A similar technique could be used in place of headers.

For the readers like me, the impurity of this suggestion might make you a bit queasy. There certainly are scenarios where this technique fits the bill just fine, and others where it should be avoided. You'll have to weight the merits of purity versus performance for your own application.

2. Enable caching on preflight responses to reduce repeat checks.

Just like with standard HTTP requests, proper caching will improve CORS performance. Unfortunately, we can't use the familiar Cache-Control header to configure preflight response caching policies.

Instead, the CORS specification introduces a new header: Access-Control-Max-Age. Access-Control-Max-Age has a simple numeric value, indicating the number of seconds in which the browser should cache a preflight response. For best effect, set the value of Access-Control-Max-Age to the longest duration that you can stomach.

Measuring Performance with Resource Timing

Of course, you aren't the only one concerned with performance. The consumers of your API and content are concerned about performance as well. As such, they may be leveraging features like Navigation Timing, Resource Timing and User Timing to track performance as experienced by their actual users. (If you are unfamiliar with these features, I highly recommend checking out my Tracking Real World Web Performance course on Pluralsight.)

For security purposes, the Resource Timing specification restricts many useful attributes from being disclosed for CORS requests, including:

  • redirectStart
  • redirectEnd
  • domainLookupStart
  • domainLookupEnd
  • connectStart
  • connectEnd
  • requestStart
  • responseStart
  • secureConnectionStart

This restriction can be lifted by adding a Timing-Allow-Origin header to responses with a value which lists all the allowed origins (in a comma delimited list), or a value of '*' to allow all origins.

While Timing-Allow-Origin doesn't affect the performance of your service the way that reducing preflight requests does, it does allow consumers to get much finer grained detail and insight into the performance you do have, and I consider that a win.

Further Reading

CORS in Action

For more information about CORS, check out CORS in Action by Monsur Hossain.