Every now and then, we realize we need to go back to the basics of building websites (read as web apps).
Yes, FEO is a real term - Front End Optimization.
TL;DR : We’ll take a quick look at how we moved to using CDNs to serve our static assets. Oh yeah - it is that simple.
Once upon a time : We were using a beefy dedicated server that never let us see these sluggish niggles. We let IIS handle everything it could and more.
Present day : We moved to the cloud with Azure as our infrastructure/service platform. We use the Azure App Service to host our web application.
Funnily enough, we never got the same feel as our dedicated box. You know - that instinctive reaction you have when you look at your own app in a different skin. That. That was off. Way off.
We did what we could to try and bring back the same beefi-ness - scaled up, scaled out, used caches for our data. Yet, it wasn’t enough.
But there was something innate that we missed. It was about the client. Yes - the browser. Chrome/Firefox.
Browsers ultimately decide when to load what and how.
So, Akshay and I took a quick look at the network tab in Dev Tools on Chrome. Akshay was quick to point out that the requests were queued and were waiting for something. We noticed the little gray notification in the bottom left of the browser - “waiting for manage.adbox.pro”. A little gooogling and some previous experience with this helped us realize that this is because Chrome only allows 6 concurrent connections per server.
And we relied on our webserver to do all the work - even serve the static assets. From the same domain! There - that was our mistake. So we were flooding the browser’s pipeline with asset requests. And that, caused our AJAX requests to be held up until assets were received.
Akshay was convinced - all we needed to do was move our assets outside the webserver; to a CDN. Azure CDN was the obvious choice. It integrated with our web app and all we needed to do was setup the CDN endpoint. The simple endpoint configuration allowed us to tell Azure CDN to get the assets from our web app’s URL. An overnighter from Akshay meant that this was ready for me to use by morning.
- The docs at Azure CDN can tell you more about the What and Why of CDNs
The next step was to ensure that all our core assets (stylesheets & scripts for now) used the CDN endpoint URLs instead of the webserver URLs (or relative path).
We were already using ASP.net’s Bundling and Minification.
How we used the default bundling and regular scripts/style tags :
<!-- Layout.cshtml -->
@System.Web.Optimization.Styles.Render("~/Content/css/platformcss")
@System.Web.Optimization.Scripts.Render("~/Content/platformjs")
<link rel="stylesheet" href="~/Content/dxDatepicker/dxDatePicker.css" type="text/css" />
.
.
.
<script src="~/Content/Areas/App/Views/Dashboard/List.js"></script>
This does have and option to use a CDN URL -
// code sample from https://www.asp.net/mvc/overview/performance/bundling-and-minification
public static void RegisterBundles(BundleCollection bundles)
{
//bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
// "~/Scripts/jquery-{version}.js"));
bundles.UseCdn = true; //enable CDN support
//add link to jquery on the CDN
var jqueryCdnPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js";
bundles.Add(new ScriptBundle("~/bundles/jquery",
jqueryCdnPath).Include(
"~/Scripts/jquery-{version}.js"));
// Code removed for clarity.
}
But that’s not what we wanted; we wanted a slightly easier way to dynamically put in our CDN Endpoint’s URL in the script/style references - while rendering it.
So I conjured up (JK; I googled most of it) an extension method to help us do just that :
public static IHtmlString RenderStyles(string path)
{
string linkTag = "<link rel=\"stylesheet\" href=\"" + ConfigurationManager.AppSettings["CDN1"] + "{0}\" type=\"text/css\" />";
return Styles.RenderFormat(linkTag, path);
}
public static IHtmlString RenderScripts(string path)
{
string scriptTag = "<script src=\"" + ConfigurationManager.AppSettings["CDN2"] + "{0}\"></script>";
return Scripts.RenderFormat(scriptTag, path);
}
And now, we use our custom extension :
<!-- Layout.cshtml -->
@DeltaX.Web.App_Start.BundleConfig.RenderStyles("~/Content/css/platformcss")
@DeltaX.Web.App_Start.BundleConfig.RenderScripts("~/Content/platformjs")
@DeltaX.Web.App_Start.BundleConfig.RenderStyles("~/Content/dxDatepicker/dxDatePicker.css")
.
.
.
@DeltaX.Web.App_Start.BundleConfig.RenderScripts("~/Content/Areas/App/Views/Dashboard/List.js?v=201611221629346572")
One of the key things about this extension was that it preserved the default MVC Bundling’s benenfits :
- Cache-busting query string parameter for bundles
- Dynamic CDN base URLs meant that in the development environment, it would render individual assets (not bundled or minified)
- We can use this extension to render other scripts/style tags that are not setup as a bundle during compile time - and we can benefit from CDN serving.
- We ensure we add the cache-busting query string :
?v=<timestamp>
- We ensure we add the cache-busting query string :
And that concludes this article. We went back to the basics and we are seeing the difference in performance on our app.
In the next couple of posts, I’ll talk about other optimizations we did in terms of ASP.net MVC. Stay tuned!