Last time we looked at the performance of my site, we discovered that the DNS was a source of the problem due to slow resolution time. In this next part, we’ll try and tackle two things at the same time.
A CDN and Cookie-Free Domains
I’m doing this a bit out-of-order compared with how I actually configured my server, this I’m presenting this in a way that will require the least amount of backtracking. If setting up a CDN seems like too much work, you can skip this one and wait for the next part (GZipping).
One of the things YSlow gave me an “E” on (wow, worse than an F) was cookie-free domains. I’m using Google Analytics to track visitors on my site, and the way it accomplishes this is with cookies.
Cookies are a perfectly valid thing these days, however setting a cookie for my domain, vcsjones.com, meant that all static content like CSS, JavaScript, and images were sent with the cookie. Analytics was setting cookies when someone hit my site, and the web server was happy enough to send along the cookie header with this static content.
The typical solution for this is to use a cookie free domain, i.e. use a different domain for your static content. I couldn’t use a subdomain like static.vcsjones.com because cookies set at vcsjones.com also apply to all child domains. My only option to using a cookie free domain was to purchase another domain and serve static content from there, like staticvcsjones.com.
However, before I did any of that, YSlow suggested I use a Content Delivery Network, or CDN, for my static content. If I moved my static content to a CDN, then I would be taking care of the cookie free domains problem as well.
On the theme of using Amazon for everything, I settled on giving CloudFront a shot. CloudFront is Amazon’s content delivery network solution.
I had a couple of choices on how I wanted to set this up.
Host my static content elsewhere (bad)
I had originally gone the route of trying to move all of my static content to Amazon’s S3 solution. CloudFront is easy to configure to serve content from an S3 bucket, but this turned out to be a troublesome approach from a maintenance standpoint. I would not only need to move all of my uploads to S3, but also the theme content, and other “innards” of WordPress. This would be difficult keeping things up-to-sync. I wasn’t able to find a WordPress plugin that could keep all of that in sync for me, and manually uploaded content to S3 seemed tiresome. It would also mean any time I upgraded a plugin, theme, or WordPress itself, I would need to move all of those files to S3 again. This didn’t seem like a workable approach.
Leave my static content as is (good)
I could leave my static content exactly where it is, and set up CloudFront to use my own server as a source of static content. This seemed like the best approach. If my file content changed, the CDN would pick it up (eventually). Any new files would instantly be picked up by CloudFront, and I wouldn’t have to change where WordPress’s physical files were. That would better for upgrading and plugin changes.
Making it all work
I had initially set up CloudFront to point to vcsjones.com. That worked well enough, CloudFront served anything from my server in its edge cache. This however, left me with a bit of a yucky feeling: any dynamic content could possibly end up in CloudFront’s cache. I wasn’t a big fan of that, so I decided to truly separate my static content from my dynamic content, even if they were all in the same place.
I first brought up a new subdomain, static.vcsjones.com. I had it pointed to the same physical location as vcsjones.com. At first, this was a true 100% copy of vcsjones.com. My next intention was to configure nginx, my server, to only serve static content from static.vcsjones.com, and block it from vcsjones.com. As a last step, I would configure CloudFront to use static.vcsjones.com as an origin.
Configuring NGINX
NGINX is a great server, and I love its flexibility and speed. My static content virtual site configuration looked like this:
server {
listen 80;
server_name static.vcsjones.com;
root /var/www/wordpress/;
location ~* \.(?:js|css|png|jpg|jpeg|gif|ico)$ {
expires max;
add_header Pragma public;
access_log off;
break;
}
location / {
return 404;
}
}
This is an abbreviated version of what I currently have running now. So static.vcsjones.com will only serve static content, which helps mitigate the possibility of an search engine finding this domain somehow and dinging my SEO rankings for serving duplicate content.
So, vcsjones.com is being used to serve dynamic content, static.vcsjones.com is used to serve only static content, and my CloudFront CDN is using static.vcsjones.com.
Finally, I blocked static content from vcsjones.com with a few exceptions.
location ~* \.(?:js|css|png|jpg|jpeg|gif|ico)$ {
if ($http_referer ~ "wp-admin") {
break;
}
if ($request_filename ~* jquery\.js$) {
break;
}
return 404;
}
This blocks static content from vcsjones.com unless it is jquery, or if the referer contains wp-admin.
jQuery is a bit of a special beast in WordPress. It doesn’t appear to be easy to cache via a CDN because WordPress dynamically builds it depending on what the extensions ask for.
wp-admin is whitelisted as a referer because we don’t want to break the admin section of WordPress, which appears to be a little more difficult to fix URLs in.
Fixing URLs in WordPress
So now with this fancy-pants CDN, I actually needed to use it. For now, I am using two WordPress plugins to accomplish this.
CDN Rewrite works well to rewrite content from WordPress’s themes and includes. This changes my theme to load its CSS, images, and JS from CloudFront. The exception to this is jQuery. jQuery in WordPress is an odd beast, it appears that WordPress dynamically builds jQuery depending on what pieces of it is needed. For that purpose, jQuery is not served over my CDN to my site, which is rather unfortunate (though it is gzipped).
Real Time Find and Replace works well to rewrite content in actual post bodies, that CDN Rewrite doesn’t seem to do.
My intention is to eventually eliminate both by fixing the actual links in post bodies, and creating a child theme. This will be a small improvement such that WordPress has less processing to do on the rendered HTML.
Due to the odd handling of jQuery in WordPress, neither of them catch the URL for jQuery, so jQuery is served outside the CDN, which is actually rather unfortunate.
CloudFront CNAME
One option CloudFront has is the ability to specify a CNAME in your DNS to CloudFront. I initially took this approach because it allowed more control over where my static content was located. If I use my CDN endpoint, dsdujlkb89x0f.cloudfront.net, that would mean if I spent all that time fixing static content URLs, I would have to fix them again if I opted to not use CloudFront. Initially I had setup cdn.vcsjones.com to point to CloudFront, but because it is a subdomain of vcsjones.com, it was getting Google Analytics cookies. Instead I opted to just use CloudFront’s domain. This keeps my static content cookie free, and also means I won’t pay for DNS lookups in Route 53.
Gotchas
This CDN approach works well for me, with a few issues that are acceptable trade offs, or is easy to work around.
Because I disabled static content on vcsjones.com to better separate the static content, the Administration bar is a little broken when used outside of wp-admin.
CloudFront has no easy to purge all of its cache. If you need purge something from CloudFront, you need to specify the path you want to purge, including the query string.
More of this series
- Scaling WordPress Part 1: DNS
- Scaling WordPress Part 2: CDN