Scott Hanselman

CDNs fail, but your scripts don't have to - fallback from CDN to local jQuery

April 30, '13 Comments [46] Posted in ASP.NET | Javascript
Sponsored By

CDN issues in the Northeast

There's a great website called http://whoownsmyavailability.com that serves as a reminder to me (and all of us) that external dependencies are, in fact, external. As such, they are calculated risks with tradeoffs. CDNs are great, but for those minutes or hours that they go down a year, they can be super annoying.

I saw a tweet today declaring that the ASP.NET Content Delivery Network was down. I don't work for the CDN team but I care about this stuff (too much, according to my last performance review) so I turned twitter to figure this out and help diagnose it. The CDN didn't look down from my vantage point.

I searched for things like "ajax cdn,"microsoft cdn," and "asp.net cdn down" and looked at the locations reported by the Twitter users in their profiles. They had locations like CT, VT, DE, NY, ME. These are all abbreviations for states in the northeast of the US. There were also a few tweets from Toronto and Montreal. Then, there was one random tweet from a guy in Los Angeles on the other side of the country. LA doesn't match the pattern that was developing.

I tweeted LA guy and asked him if he was really in LA or rather on the east coast.

Bingo. He was VPN'ed into Massachusetts (MA). I had a few folks send me tracerts and sent them off to the CDN team who fixed the issue in a few minutes. There was apparently a bad machine in Boston/NYC area that had a configuration change specific to the a certain Ajax path that had gone undetected by their dashboard (this has been fixed and only affected the Ajax part of the CDN in this local area).

More importantly, how can we as application developers fallback gracefully when an external dependency like a CDN goes down? Just last week I moved all of my Hanselminutes Podcast images over to a CDN. If there was a major issue I could fall back to local images with a code change. However, if this was a mission critical site, I should not only have a simple configuration switch to fallback to local resources, but I should also test and simulate a CDN going down so I'm prepared when it inevitably happens.

With JavaScript we can detect when our CDN-hosted JavaScript resources like jQuery or jQuery UI aren't loaded successfully and try again to load them from local locations.

Falling back from CDN to local copies of jQuery and JavaScript

The basic idea for CDN fallback is to check for a type or variable that should be present after a script load, and if it's not there, try getting that script locally. Note the important escape characters within the document.write. Here's jQuery:

<script src="http://ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js"></script>
<script>
if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='/js/jquery-2.0.0.min.js' type='text/javascript'%3E%3C/script%3E"));
}
</script>

Or, slightly differently. This example uses protocol-less URLS, checks a different way and escapes the document.write differently.

<script src="//ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js"></script>
<script>window.jQuery || document.write('<script src="js/jquery-2.0.0.min.js">\x3C/script>')</script>

If you are loading other plugins you'll want to check for other things like the presence of specific functions added by your 3rd party library, as in "if (type of $.foo)" for jQuery plugins.

Some folks use a JavaScript loader like yepnope. In this example you check for jQuery as the complete (loading) event fires:

yepnope([{
load: 'http://ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js',
complete: function () {
if (!window.jQuery) {
yepnope('js/jquery-2.0.0.min.js');
}
}
}]);

Even better, RequireJS has a really cool shorthand for fallback URLs which makes me smile:

requirejs.config({
enforceDefine: true,
paths: {
jquery: [
'//ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min',
//If the CDN location fails, load from this location
'js/jquery-2.0.0.min'
]
}
});

//Later
require(['jquery'], function ($) {
});

With RequireJS you can then setup dependencies between modules as well and it will take care of the details. Also check out this video on Using Require.JS in an ASP.NET MVC application with Jonathan Creamer.

Updated ASP.NET Web Forms 4.5 falls back from CDN automatically

For ASP.NET Web Forms developers, I'll bet you didn't know this little gem. Here's another good reason to move your ASP.NET sites to ASP.NET 4.5 - using a CDN and falling back to local files is built into the framework.

(We've got this for ASP.NET MVC also, keep reading!)

Fire up Visual Studio 2012 and make a new ASP.NET 4.5 Web Forms application.

When using a ScriptManager control in Web Forms, you can set EnableCdn="true" and ASP.NET will automatically change the <script> tags from using local scripts to using CDN-served scripts with local fallback checks included. Therefore, this ASP.NET WebForms ScriptManager:

<asp:ScriptManager runat="server" EnableCdn="true">
<Scripts>
<asp:ScriptReference Name="jquery" />
<asp:ScriptReference Name="jquery.ui.combined" />
</Scripts>
</asp:ScriptManager>

...will output script tags that automatically use the CDN and automatically includes local fallback.

<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.js" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
(window.jQuery)||document.write('<script type="text/javascript" src="Scripts/jquery-1.8.2.js"><\/script>');//]]>
</script>

<script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.24/jquery-ui.js" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
(!!window.jQuery.ui && !!window.jQuery.ui.version)||document.write('<script type="text/javascript" src="Scripts/jquery-ui-1.8.24.js"><\/script>');//]]>
</script>

What? You want to use your own CDN? or Googles? Sure, just make a ScriptResourceMapping and put in whatever you want. You can make new ones, replace old ones, put in your success expression (what you check to make sure it worked), as well as your debug path and minified path.

var mapping = ScriptManager.ScriptResourceMapping;
// Map jquery definition to the Google CDN
mapping.AddDefinition("jquery", new ScriptResourceDefinition
{
Path = "~/Scripts/jquery-2.0.0.min.js",
DebugPath = "~/Scripts/jquery-2.0.0.js",
CdnPath = "http://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js",
CdnDebugPath = "https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.js",
CdnSupportsSecureConnection = true,
LoadSuccessExpression = "window.jQuery"
});

// Map jquery ui definition to the Google CDN
mapping.AddDefinition("jquery.ui.combined", new ScriptResourceDefinition
{
Path = "~/Scripts/jquery-ui-1.10.2.min.js",
DebugPath = "~/Scripts/jquery-ui-1.10.2.js",
CdnPath = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js",
CdnDebugPath = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.js",
CdnSupportsSecureConnection = true,
LoadSuccessExpression = "window.jQuery && window.jQuery.ui && window.jQuery.ui.version === '1.10.2'"
});

I just do this mapping once, and now any ScriptManager control application-wide gets the update and outputs the correct fallback.

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.js" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
(window.jQuery)||document.write('<script type="text/javascript" src="Scripts/jquery-2.0.0.js"><\/script>');//]]>
</script>

<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.js" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
(window.jQuery && window.jQuery.ui && window.jQuery.ui.version === '1.10.2')||document.write('<script type="text/javascript" src="Scripts/jquery-ui-1.10.2.js"><\/script>');//]]>
</script>

If you want to use jQuery 2.0.0 or a newer version than what came with ASP.NET 4.5, you'll want to update your NuGet packages for ScriptManager. These include the config info about the CDN locations. To update (or check your current version against the current) within Visual Studio go to Tools | Library Package Manager | Manage Libraries for Solution, and click on Updates on the left there.

image

Updated ASP.NET Web Optimization Framework includes CDN Fallback

If you're using ASP.NET MVC, you can update the included Microsoft.AspNet.Web.Optimization package to the -prerelease (as of these writing) to get CDN fallback as well.

Get Optimization Updates by "including PreRelease"

Note that I've on the Updates tab within the Manage NuGet Packages dialog but I've selected "Include Prerelease."

Now in my BundleConfig I can setup my bundles to include not only the CdnPath but also a CdnFallbackExpression:

public static void RegisterBundles(BundleCollection bundles)
{
bundles.UseCdn = true;
BundleTable.EnableOptimizations = true; //force optimization while debugging

var jquery = new ScriptBundle("~/bundles/jquery", "//ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js").Include(
"~/Scripts/jquery-{version}.js");
jquery.CdnFallbackExpression = "window.jQuery";
bundles.Add(jquery);
//...
}

Regardless of how you do it, remember when you setup Pingdom or other availability alerts that you should be testing your Content Delivery Network as well, from multiple locations. In this case, the CDN failure was extremely localized and relatively short but it could have been worse. A fallback technique like this would have allowed sites (like mine) to easily weather the storm.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Tuesday, April 30, 2013 8:18:15 PM UTC
Yeah! That's true, even i use to follow this Fallback method by configuring the value to "True" or "False". I loved the above code snippets, very comfortable for every web developer :-)
Tuesday, April 30, 2013 8:22:29 PM UTC
Awww - is it wrong I was totally hoping that whoownsmyavailability.com would scan all my sites and show me all my points of failures in a neat bar graph?
Fergal Moran
Tuesday, April 30, 2013 8:27:25 PM UTC
Is it possible to setup multiple fallbacks in ScriptManger? say first MS then Google then code.jquery.com then local?
Paul Chen
Tuesday, April 30, 2013 8:39:15 PM UTC
Paul - No, just CDN, then local. The idea being that you really want to get the file to the user, not test around at a bunch of CDNs.
Tuesday, April 30, 2013 9:00:19 PM UTC
You may also want to take into account a fallback for the jQuery UI css file also, as you are more than likely to be using a CDN for this also.

I have blogged about it here jQuery and jQuery UI fallbacks

You can check for the visibility of an element with the class="ui-helper-hidden". If it is visible, then your CSS has not loaded and can fall back to the local version.
Tuesday, April 30, 2013 9:08:03 PM UTC
Tim - Cool. You can also look at the sheets themselves in the DOM and check their rules. http://stackoverflow.com/a/7452378
Tuesday, April 30, 2013 9:15:58 PM UTC
Scott,
In at least the last 10 sites I've built the only one that didn't use a CDN AND Local fallback was for a privately secure site where, due to security levels, we removed the CDN call from an HTML5 Boilerplate based template and only used the local call?

I'm just wondering why anyone wouldn't use both scripting calls as best practices ... I'm confused?

Jon
Jon Humphrey
Tuesday, April 30, 2013 9:21:27 PM UTC
Jon - Folks just don't think about it. They copy in a CDN and go to production.
Tuesday, April 30, 2013 9:21:39 PM UTC
It also makes sense to implement a circuit breaker to prevent a UX degradation, since every client would wait for CDN's timeout.
Tuesday, April 30, 2013 9:25:01 PM UTC
Hello Scott,

thank you for great article, just small correction. RequireJS module paths should not end with .js extension.
Michal Brndiar
Tuesday, April 30, 2013 9:31:32 PM UTC
@Scott, hopefully they'll read this post and pull their collective head out of their ass the sand! :-D

@Ivan, thank you! I wouldn't have ever called that situation a "circuit breaker", you learn something new every day!

Jon
Jon Humphrey
Tuesday, April 30, 2013 9:33:33 PM UTC
Michal - Thanks, fixed
Tuesday, April 30, 2013 9:48:06 PM UTC
I've been following the HTML5 Boilerplate discussion (https://github.com/h5bp/html5-boilerplate/pull/1327) about whether or not jQuery should be loaded with a CDN at all. I love the local fallback snippets here but I'm starting to wonder if using a CDN for constantly changing scripts add any tangible benefit.
Tuesday, April 30, 2013 10:46:27 PM UTC
Scott,
are any other the additional path options in scriptmanager also available with MVC4's bundle system? i.e. Path, CdnPath,DebugPath, CdnDebugPath. I'm only aware of the first 2.
Cheers
Tim
,
Tim Bertenshaw
Tuesday, April 30, 2013 11:48:51 PM UTC
What if your local server(s) couldn't handle the load without CDN support?
Steve
Wednesday, May 01, 2013 12:24:58 AM UTC
I believe you could do something similar with images using the error event: http://www.quirksmode.org/dom/events/error.html


An example is http://stackoverflow.com/a/92819/209136 but instead of showing an error image you could modify the src value to go to the local copy.
Jonathan
Wednesday, May 01, 2013 1:58:24 AM UTC
Thanks for the insight. :P
Wednesday, May 01, 2013 5:57:56 AM UTC
looking for the predefined mappings.
Wednesday, May 01, 2013 6:13:33 AM UTC
Steve - If your local servers can't serve a few 20k static files, you have much larger issues.

Hannes - Pardon? The code is open. Crack open the NuGet packages, they are ZIPs.
Wednesday, May 01, 2013 7:09:50 AM UTC
Great article Scott, thank you.
Wednesday, May 01, 2013 7:56:41 AM UTC
+1 for Tim's question : I can see the CDN options in MVC Bundling, but I don't think that auto-fails (without writing it in obv). Any chance this is coming to MVC?

Thanks for the usefulness.

-tom
Wednesday, May 01, 2013 8:46:34 AM UTC
Hello, Tom!

In ASP.NET MVC is already available this feature. You just need to install the pre-release version of Microsoft ASP.NET Web Optimization Framework (now available is a version 1.1.0 Beta 1). More in detail about this can be read in the Howard Dierking's article «New Web Optimization Pre-Release Package on NuGet»
Wednesday, May 01, 2013 8:54:45 AM UTC
QUOTE: "I don't work for the CDN team but I care about this stuff (too much, according to my last performance review)"

Scary. Speaking from experience, caring too much can get you fired.

Thank you for this article, Scott ... i really appreciate the heads up ... so many gotchas to get us; in his excellent ASP.NET MVC free pluralsight videos*, k. Scott Allen mentions the benefits of CDN but i do not recall him mentioning this hazard ... that's not a criticism of kSA because one can only stuff so much information into a video before it becomes an alternative to L. D. Groban's 87 hour "Cure for insomnia". * http://www.asp.net/mvc "Essential Videos"

one point: <noscript>
http://www.w3.org/TR/html-markup/noscript.html`
http://www.w3.org/html/wg/drafts/html/master/scripting-1.html#the-noscript-element

FWIW, a CDN does nothing at all, ditto workarounds, when scripting itself is turned off ... many developers seem to take a "so what" attitude, a.k.a., blame the end user.

BTW, the TLA CDN in another non-TLA context is used to denote "Canadian currency", ergo, at first glance upon your title, i was wondering why Canadians fail.

/g.
Wednesday, May 01, 2013 10:18:33 AM UTC
Hi Scott,
Jus' something ran in my mind when @Paul Chen asked about...

Is it possible to setup multiple fallbacks in ScriptManger? say first MS then Google then code.jquery.com then local?

Yes Paul can follow this way, if he prefers multiple fallback(not recommended by any Pro's), but can play around like this...

1st way is using Microsoft's CDN, the alternate way of 1st condition is to use Google CDN or having a fallback to the native local file system javascript code.

<script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.min.js"></script>
<script type="text/javascript">
if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js' type='text/javascript'%3E%3C/script%3E"));
}
else if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='/js/jquery-1.4.2.min.js' type='text/javascript'%3E%3C/script%3E"));
}
</script>


Hope this helps...
Cheers!
Guruprasad.
Wednesday, May 01, 2013 3:25:18 PM UTC
Guruprasad, I don't think that would work. I think the two fallbacks would need to be in separate script blocks (to give document.write a chance to work), and obviously the second branch should be inside an if, not an else if.
Max
Wednesday, May 01, 2013 3:41:11 PM UTC
This is *very* slick. I'm impressed that ASP.NET does this by default too.

-- An entrenched Ruby on Rails developer
Wednesday, May 01, 2013 4:39:44 PM UTC
Guruprasad - That's manual, not handled by ScriptManager. You can always do things manually, which is useful, but ScriptManager doesn't handle double fallback.

Ian - Glad you liked that!
Wednesday, May 01, 2013 4:58:31 PM UTC
Why do you need escape characters in document.write?
littleguy
Wednesday, May 01, 2013 5:44:56 PM UTC
Hi Scott,

Nice article, thanks.
A few questions:
1. Would the built-in ScriptManager & Optimization framework CND capabilities work properly in both http & https pages?
2. Am I correct that the Optimization framework could be used in any .NET 4.0 web project, not just MVC as you mentioned?
Michael
Wednesday, May 01, 2013 7:08:29 PM UTC
I love rails assets compilation.

Why bother with cdn,
I am a rails guy.
So in rails sprockets includes and minifies all js files by default when in production.

So a single javascript server my entire purpose.

Things single javascript has all my javascript,jquery and other js files.

I am not sure if the idea to roll all javascripts in a ball has any flaws as such but I certainly need not worry about jquery not being loaded.


Sethu
Wednesday, May 01, 2013 8:41:07 PM UTC
Nice. It could definitely be useful to write fallback scripts such as these.

In the beginning of the article you mentioned reaching out to various people to determine what areas were experiencing an outage. I just thought I'd mention that there's a great tool that allows you to test from different locations all over the world. It's at http://www.webpagetest.org/ Currently they have 10 locations in the US.

@Sethu - A CDN is normally a pretty reliable thing. And the benefit comes in the form of reduced latency. The payoff is bigger when you have a global audience. Lastly, when it comes to jQuery the best option is to use Google's hosted version. The reason is that so many sites reference Google's version that the user is likely to have it cached in their browser before they ever reach your site. That's something I cover in my article here... http://blog.bucketsoft.com/2012/03/maximize-your-chances-of-caching-your.html
Thursday, May 02, 2013 3:04:49 AM UTC
A nice approach, but it relies on the remote script retrieval failing fast. If the remote server fails to respond in a timely manner, your page is going to block on that script before it gets to the fallback - I've seen this problem on several occasions with Google Analytics' ga.js.

Liam Clarke
Thursday, May 02, 2013 3:33:27 AM UTC
@Liam - I thought about that too. For that reason I took another approach to this problem awhile back.

I wrote .NET code utilizing the Timer class to check the uptime of an external source by polling it every 2 minutes. By using the Timer, this code will execute on a separate background thread and will never slow down an ASP.NET request. So whenever the external source is down then I set a flag (a static variable) which when false will disable the script, or in this case failover to an internal version.
Thursday, May 02, 2013 2:47:11 PM UTC
I shall move to 4.5

once again, ms, save me time

:)
Thursday, May 02, 2013 6:27:20 PM UTC
Is it worth having the fallback, though? I imagine in a lot of respects that if a CDN is down then there's gonna be a 60+ second timeout that has to occur before the fallback can be loaded. For some types of sites(blogs, etc), that delay can be the difference between whether a user stays on your site or backs out and goes elsewhere. If it matters, I would think the site would just use a local resource all the time.
Saturday, May 04, 2013 3:07:03 PM UTC
Great article Scott !
Learn't a new thing, will be including it in my best practices. :)
Bibhu
Sunday, May 05, 2013 9:30:29 PM UTC
@Steve Wortham: if your CDN check runs on the server it is useless as it will not detect if just one of the server of the CDN is down. The right approach is to do it client-side as in the article.
Olivier Mengué
Tuesday, May 07, 2013 5:00:14 AM UTC
Very good post sir! Not only for ASP.NET solutions, but also with some general solution for all web apps. We had a internet outage yesterday and the team ran into sorta chaos ;)
Tuesday, May 07, 2013 3:24:56 PM UTC
Awesome!
Thanks for sharing such a nice solution.
Wednesday, May 08, 2013 1:45:51 AM UTC
My daily work is n Asp.Net MVC . This helped me learn a new and important thing.Thanks
Venkatesh kadiri
Sunday, May 12, 2013 2:51:09 PM UTC
@Olivier - Ah yes, good point. That solution worked well for us because the external service we were testing wasn't actually on a CDN. I forgot about that.
Monday, May 27, 2013 1:42:45 PM UTC
Hello Scott,

I confuse why ScriptManager control automatically generates script does not use protocol-less URLS.
Monday, June 17, 2013 12:28:05 AM UTC
Great article and yes it is important we consider these issues in our application. In case any rails guys see this thread, here a few helpful gems I created to address this concern.
jquery-rails-google-cdn and jquery-ui-rails-google-cdn.
Friday, November 01, 2013 7:27:50 PM UTC
Hi, Scott!
I work at ISP in Russia, and our customers have similar problems with ajax.aspnetcdn.com.
Can you tell me the contacts of Microsoft CDN team?
Thanks in advance for your help!
Alex
Thursday, November 21, 2013 3:45:26 PM UTC
I can't express how surreal it is to randomly come across a blog post and find your own tweet in it.

Thanks for the great info!
Attila DeLisle
Thursday, November 28, 2013 3:23:52 PM UTC
Nice article,
Some info here : Pageload optimization using jQuery CDN http://markupjavascript.blogspot.com/2013/11/pageload-optimization-using-jquery-cdn.html
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.