Lately, I have been obsessed focused on how to get the page load times faster on this site. The server running this site is underpowered to say the least, so I've implemented some caching strategies on the MVC routes (see "Faster Page Load..." post).
In order to spread the load of this caching out to more than just my little mini pc, I have employed the services of Cloudflare with one of their free accounts. Thanks Cloudflare! Even with a free account, there are a ton of config options for how to cache your site's content. Connecting your site to Cloudflare is done by simply changing your domain's nameservers to use Cloudflare's nameservers. Stupid simple.
Cloudflare's free account includes up to three "Page Rules" which allow you to customize caching settings in three different ways.
See one Page Rule example below:
The results have been interesting, but the introduction of Cloudflare alone did not yield as much caching as I would have hoped. One major point of contention I have dealt with was the way ASP.NET bundling of css and js files seemed to defy all attempt to be cached. No matter what I tried (forcing cache-control headers, overriding caching policy per request, customising bundle URLs to include extensions???) I could not get the response headers to correctly cache these static assets. Side note: rather than inspecting response headers in chrome dev tools, I found and used a site to quickly check cached pages (giftofspeed.com/cache-checker).
In order to overcome the impossible-to-cache ASP.NET bundling, I landed on implementing a custom helper class which I creatively named BundleHelper. I did not want to completely move away from the bundle feature, as it nicely concatenates the assets into one file and tidies up the .cshtml.
Basically, this helper grabs the latest bundle and insures there is a static file on the server that matches.
public static class BundleHelper
{
public static string RenderBundle(string bundle,string extension)
{
string domainName = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority);
var url = BundleTable.Bundles.ResolveBundleUrl(bundle);
var key = url.Replace(bundle.Remove(0,1)+"?v=", "");
var filestore = HttpContext.Current.Server.MapPath("/bundle");
var path = Path.Combine(filestore, string.Format("{0}.{1}", key,extension));
if (!File.Exists(path))
{
Directory.CreateDirectory(filestore);
try { Directory.EnumerateFiles(filestore, "*." + extension).ForEach(f => File.Delete(f)); } catch { }
var content = new WebClient().DownloadString(domainName + url);
File.WriteAllText(path, content);
}
return RelativeFromAbsolutePath(path);
}
static string RelativeFromAbsolutePath(string path)
{
if (HttpContext.Current != null)
{
var request = HttpContext.Current.Request;
var applicationPath = request.PhysicalApplicationPath;
var virtualDir = request.ApplicationPath;
virtualDir = virtualDir == "/" ? virtualDir : (virtualDir + "/");
return path.Replace(applicationPath, virtualDir).Replace(@"\", "/");
}
throw new InvalidOperationException("HttpContext is required.");
}
}
With the creation of the helper, there is a slight change to the page(s) that used the bundles. Here is one example of the jquery bundle, note the commented out code that is replaced by the new helper:
<script src="@BundleHelper.RenderBundle("~/bundles/jquery","js")" type="text/javascript"></script>
@*@Scripts.Render("~/bundles/jquery")*@
Now all of my site's assets are cached as desired.
Google's PageSpeed insights now reports great results as well.