How to create fallback for CDN libraries without using document.write() - javascript

I want to include 3rd party libraries, such as jQuery, from CDN. I also want to create a fallback so if the CDN fails, I include my own local copy. I have followed the suggestion here:
This is how I include jQuery in my page:
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script>window.jQuery || document.write('<script src="/Scripts/jquery-3.3.1.min.js"><\/script>');</script>
At the same time Google is saying that document.write() is unreliable and should not be used:
Using document.write() can delay the display of page content by tens
of seconds and is particularly problematic for users on slow
connections. Chrome therefore blocks the execution of document.write()
in many cases, meaning you can't rely on it.
Is there any alternative method to create fallback for the CDNs?

If you don't mind loading it asynchronously you can do it like this:
function fallback() {
var element = document.createElement('script');
element.type = 'text/javascript';
element.src = 'https://code.jquery.com/jquery-3.3.1.min.js'; // or your path to your local script
document.body.appendChild(element);
}
window.jQuery || fallback();
setTimeout(function() {
console.log(window.jQuery);
}, 1000); // Add timeout since script is loaded asynchronously

I recommend using 3p packages like fallback.js or require.js given they are more scalable in case you have multiple fallbacks and they give you faster loading performance.
Example of fallback.js
HTML CODE
<html>
<head>
<!-- **The `data-main` attribute tells the library to load `main.js`** -->
<script async data-main="main" src="fallback.min.js" type="text/javascript"></script>
</head>
<body>
</body>
</html>
MAIN JS FILE
cfg({
"libs": {
"jQuery": {
"urls": [
"//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min",
"//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min"
]
},
}
});
req(function(jQuery) {
jQuery("body");
});
Example of require.js
requirejs.config({
enforceDefine: true,
paths: {
jquery: [
'https://code.jquery.com/jquery-3.4.1.min.js',
//If the CDN location fails, load from this location
'js/jquery-3.4.1.min.js'
]
}
});
require(['jquery'], function ($) {});

Related

How to load multiple files from CDNs asynchronously (but execute them synchronously)?

When downloading multiple commonly used javascript/css files (e.g. boostrap and jquery), many topics like this one recommend the use of a CDN, with one of the main arguments that it can then be used to load them asynchronously.
How does that work? To the best of my knowledge, <script> tags in the header are read synchronously, so it won't actually look at the second CDN file until the first one is finished.
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
How can I make the page download the scripts asynchronously, but execute them synchronously? Or is that actually happening by default somehow? And what about CSS files, will my
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
behave any different in that sense? I would like to understand the loading process properly before adding my own failovers to local code (for if the CDN is down), as to prevent getting stuck with synchronous downloading.
(Note that, despite the near-identical title, this is not a duplicate of this question, which is about loading scripts dynamically.)
Also note that I can't use defer (at least in the vanilla way that I know) as that would prevent me from adding said failover when the CDN is down, e.g.
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.2.1/js/bootstrap.min.js"></script>
<script> $.fn.modal || document.write('<script src="Script/bootstrap.min.js">\x3C/script>')</script>
would be broken by simply adding defer.
It's more about parallelism than asynchronousness. (They're certainly related, but the CDN argument related to limits on multiple downloads from the same origin is about parallelism.)
How can I make the page download the scripts asynchronously, but execute them synchronously?
Any decent browser, when given the three script tags you've shown, will download them in parallel (up to its parallel-from-the-same-site limit) and then execute them in order. You don't have to do anything to make that happen. Browsers read ahead in the HTML to find resources to fetch.
Adding fallback scripts with document.write might complicate the browser's ability to do that, or even prevent it, but you can ensure it declaratively using <link rel="preload" as="script" href="..."> (more on MDN). Combining that with fallback scripts for failed CDN resources, it might look something like this:
<head>
<!-- ... -->
<link rel="preload" as="script" href="//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js">
<link rel="preload" as="script" href="//cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js">
<link rel="preload" as="script" href="//maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js">
</head>
<body>
<!-- ... -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>if (!/*loaded condition*/) document.write(/*fallback*/);</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script>if (!/*loaded condition*/) document.write(/*fallback*/);</script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>if (!/*loaded condition*/) document.write(/*fallback*/);</script>
</body>
</html>
Note that that doesn't preload the fallbacks. You could, but then you'd be loading them even when the CDN was working, which wastes the end user's bandwidth. The fallbacks would be for the presumably-temporary degraded situation where the CDN was unavailable, where a degraded user experience is probably okay. (You could even show the user an indicator of a problem when scheduling the fallback, like Gmail's "something is taking longer than usual" indicator.)
If you're bothered by repeating the URLs and you're okay with document.write in small doses (as you seem to be), you can avoid duplicating the URLs by doing something along these lines:
<head>
<!-- ... -->
<script>
var scripts = [
{
url: "//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js",
okay: function() { return /*check it loaded*/; }
},
{
url: "//cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js",
okay: function() { return /*check it loaded*/; }
},
{
url: "//maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js",
okay: function() { return /*check it loaded*/; }
},
];
scripts.forEach(function(script) {
document.write('<link rel="preload" as="script" href="' + script.url + '">');
});
</script>
</head>
<body>
<!-- ... -->
<script>
scripts.forEach(function(script, index) {
var fallback = script.url.substring(script.url.lastIndexOf('/') + 1);
document.write('<script src="' + script.url + '"><\/script>');
document.write('<script>if (!scripts[' + index + '].okay()) document.write(\'<script src="' + fallback + '"><\\/script>\');<\/script>');
});
</script>
</body>
</html>
(Since that's all inline script you're unlikely to transpile, I've kept the syntax to ES5 level in case you have to support obsolete environments.)
I think you can still use defer, just put your fallback code into an event handler...
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
defer
This Boolean attribute is set to indicate to a browser that the
script is meant to be executed after the document has been parsed, but
before firing DOMContentLoaded.
Scripts with the defer attribute will prevent the DOMContentLoaded
event from firing until the script has loaded and finished evaluating.
[...]
Scripts with the defer attribute will execute in the order in which
they appear in the document.
... so DOMContentLoaded could be a good pick.
Or, you can also put the fallback code into a separate .js file, and then it can be loaded with defer too, relying on the bottom part of the quotation, so the in-order execution.

add crossorigin attribute to script tag with require js

here my problem,
I call a lot of script with requirejs and for one of them (and ONLY one) I need to add crossorigin attribute to make my script call look like that
<script type="text/javascript" charset="utf-8" async
data-requirecontext="_" data-requiremodule="myModule"
src="cdn.moduleUrl" crossorigin="anonymous"></script>
I've already search on docs, google and stackOverflow but I could'n find any response.
P.S.: I use requirejs 2.3.5
Ok, so I finnaly found a "proper" solution to my problem, in require code, you have an undocumented option onNodeCreated to set a callback before inserting script tag on dom so with this code, I achieve my goal !
require.config({
paths: {
'myModule': 'cdn.moduleUrl'
},
attributes: {
"myModule": {
crossorigin: "anonymous"
}
},
onNodeCreated: function(node, config, name, url){
if(config.attributes && config.attributes[name]){
Object.keys(config.attributes[name]).forEach(attribute => {
node.setAttribute(attribute, config.attributes[name][attribute]);
});
}
}
});

How to defer inline Javascript?

I have the following html code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/blazy/1.8.2/blazy.min.js" defer></script>
<script src="https://code.jquery.com/jquery-2.1.4.min.js" integrity="sha256-8WqyJLuWKRBVhxXIL1jBDD7SDxU936oZkCnxQbWwJVw=" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.9.0/js/lightbox.min.js" defer></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous" defer></script>
<!-- 26 dec flexslider js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/flexslider/2.6.3/jquery.flexslider.min.js" defer></script>
<script defer>
(function($) {
$(document).ready(function() {
//do something with b-lazy plugin, lightbox plugin and then with flexslider
});
})(jQuery);
</script>
</head>
<body>
</body>
</html>
I get an error, saying jQuery is not defined. Now even if I remove defer from my inline JS code, it says jQuery is undefined. For some reason I have to keep the jQuery plugins in the head and keep my JS code inline. My question is:
Why doesn't inline Javascript code get deferred when defer attribute is present on it?
Is there a way to imitate the defer behavior on my inline Javascript code? I can put that at the end of body tag if required.
The scripts with the defer attribute load in the order they are specified, but not before the document itself has been loaded. As defer has no effect on script tags unless they also have the src attribute, the first script that gets executed is your inline script. So at that time jQuery is not loaded yet.
You can solve this in at least two ways:
Put your inline script in a .js file and reference it with a src attribute (in addition to the defer attribute which you already had there), or
Let your inline script wait for the document and the deferred scripts to be loaded. The DOMContentLoaded event will fire when that has happened:
<script>
window.addEventListener('DOMContentLoaded', function() {
(function($) {
//do something with b-lazy plugin, lightbox plugin and then with flexslider
})(jQuery);
});
</script>
NB: Notice that in the latter case $(document).ready(function() is not included any more, as that would wait for the same event (DOMContentLoaded). You could still include it like you had in your original code, but then jQuery would just execute the callback immediately, which makes no practical difference.
You can create a Base64 URL out of the script and put it into the src!
<script src="data:text/javascript;base64,YWxlcnQoJ0hlbGxvIHdvcmxkIScpOw=="
defer>
</script>
I built a quick test to see it in action.
You should see an alert with Hello world! last if defer is working:
<script defer>
alert('Why no defer?!?');
</script>
<!-- alert('Hello world!'); -->
<script src="data:text/javascript;base64,YWxlcnQoJ0hlbGxvIHdvcmxkIScpOw=="
defer></script>
<script>
alert('Buh-bye world!');
</script>
Doing it manually is a little laborious so if you have the luxury of compiling your HTML in some way (Handlebars, Angular, etc.) then that helps a lot.
I'm currently using:
<script src="data:text/javascript;base64,{{base64 "alert('Hello world!');"}}"
defer>
</script>
You can also use type="module":
<meta charset="utf-8">
<script type="module">
let t = document.getElementById('top');
console.log(t);
</script>
<h1 id="top">Top Questions</h1>
https://developer.mozilla.org/docs/Web/HTML/Element/script#attr-type
From MDN docs:
defer
This Boolean attribute is set to indicate to a browser that the script is meant to be executed after the document has been parsed, but before firing DOMContentLoaded. The defer attribute should only be used on external scripts.
This is called an IIFE (Immediately Invoked Function Expression) which gets executed before DOM is available. So, in that case jQuery is undefined because it it not in the DOM.
defer loading with plain text Data URI - Chrome and FF
#noLib #vanillaJS
suggest not to use on Cross Browser PRODuction
until MS IE dies and MS Edge will adopt the Chromium open source ;)
the only way to defer script is external file or Data_URI (without using event DOMContentLoaded)
defer
spec script#attr-defer (MDN web docs): "This attribute must not be used if the src attribute is absent (i.e. for inline scripts), in this case it would have no effect.)"
Data_URI
spec Data_URI
with right type "text/javascript" there is no need to base64 at all... ;)
using plain text so you can use simple:
<script defer src="data:text/javascript,
//do something with b-lazy plugin, lightbox plugin and then with flexslider
lightbox.option({
resizeDuration: 200,
wrapAround: true
})
">
yes, it's little bit weird hack, but <script type="module"> are deferred by default, there is no other option to mix following in exact order:
module external files - deferred by default
module inline scripts - deferred by default
external files - optionally deferred
inline scripts - only with this hack - as I know (without libraries/frameworks)
Defer/async script tags are not good enough
There is a common knowledge that you should use <script src=".." async defer> (or set script.async = true before assigning src, when you do it from JS) and/or put your scripts at the very bottom of the page, so that as much as possible of the page gets loaded and rendered to the user, as fast as possible.
defer.js (note: I am the author of this script) is written in plain JavaScript, making lazy-loading other contents more fast and performant. You can defer any javascript files as well as inline script blocks efficiently.
If your page is just an HTML page enhanced with some JavaScript, then you're good with just <script async>. It takes time for browser to parse and execute those scripts, and each UI change may reflow your layout, make your load speed more slow, no one likes staring at a blank white page; users are impatient and will leave quickly.
In various cases, using async or defer does not deliver faster page speed than defer.js does.
I checked all the proposed solutions but all have their disadvantages. So I invented my own.
Put this inline script into your head tag or right after the start of body tag:
<script>var Defer = []; document.addEventListener('DOMContentLoaded', function() { while (Defer.length) Defer.shift().call(); }); </script>
This one liner will collect all the inline scripts you want to defer and run them respectively as soon as document is fully loaded. Now anytime you need to run an inline script deferred, just register it like:
<script>
alert('This alert will show immediately.');
Defer.push(function() {
alert('This alert will show only after document is loaded.');
// You can use anything which is not loaded yet, like jQuery
$(".selector").doSomeJqueryStuff();
});
// You can use it as many times as you like and in any place of your DOM.
Defer.push(function() {
// Any inline code you want to defer
});
</script>
This inline script will run only after document is loaded. That means you can run inline jQuery script having your jQuery stay at the end of your DOM.
You can use this data url as src attribute
data:application/javascript,eval(document.currentScript.textContent)
which takes this current script tag and evaluate its content as if it was inside an external file.
it also works with lazy attribute.
it uses document.currentScript which not supported by IE browsers.
<script defer src="https://cdn.jsdelivr.net/npm/vue"></script>
<script defer src="data:application/javascript,eval(document.currentScript.textContent)">
console.log('defered', typeof Vue); // function
</script>
<script>
console.log('not defered', typeof Vue); // undefined
</script>
There is a somewhat less obscure way to accomplish deferral that does not require callbacks, promises, or data urls ... although it does a little DOM manipulation in the background. The tiny library (109 bytes compressed/gziped) https://www.npmjs.com/package/deferscript let's you do this. The example below is based on the original post.
<script src="https://cdnjs.cloudflare.com/ajax/libs/flexslider/2.6.3/jquery.flexslider.min.js" defer>
</script>
<script src="./deferscript.js" defer>
(function($) {
$(document).ready(function() {
//do something with b-lazy plugin, lightbox plugin and then with flexslider
});
})(jQuery);
</script>
All you have to do is insert a src attribute with the value ./deferscript.js.
If the problem is that jQuery variable $ is not defined, maybe you can create a fake $ function that returns a ready function waiting for the DOMContentLoaded?
All my inline scripts has $(document).ready(..... and the problem is that $ is not defined as the header scripts are deferred.
So, just add a fake $ in an inline script in head:
<script type="text/javascript">
var $ = function(element) {
return {
ready: function(callback) {
// in case the document is already rendered
if (document.readyState!="loading") callback();
// modern browsers
else if (document.addEventListener)
document.addEventListener("DOMContentLoaded", callback);
// IE <= 8
else document.attachEvent("onreadystatechange", function(){
if (document.readyState=="complete") callback();
});
}
};
};
</script>

Js stop scripts tags from running on condition

I want some of my scripts in my index.html page to run only on condition.
For example on specific version of IOS i want that the scripts after general.js not to run.
<script src="general.js"></script>
// stop here on condition
<script src="vendor/jquery/dist/jquery.js"></script>
<script src="vendor/angularjs/angular.js"></script>
<script src="vendor/angular-sanitize/angular-sanitize.js"></script>
<script src="vendor/angular-ui-select/dist/select.js"></script>
How can i do this?
RequireJS?
Why don't you use something like RequireJS. Have your main bootstrap script (your general.js) load all other scripts that depend on your condition.
You can't do that. Instead, insert the first general.js script as you would normally, and in your head, put a script that adds the files:
<script>
var scripts = ['vendor/jquery/dist/jquery.js', 'vendor/angularjs/angular.js',
'vendor/angular-sanitize/angular-sanitize.js', 'vendor/angular-ui-select/dist/select.js'];
if(condition) {
for(i=0;i<scripts.length;i++) {
$('head').append("<script src='"+scripts[i]+"'></script>");
}
}
</script>
Or in vanilla JS:
<script>
var scripts = ['vendor/jquery/dist/jquery.js', 'vendor/angularjs/angular.js',
'vendor/angular-sanitize/angular-sanitize.js', 'vendor/angular-ui-select/dist/select.js'];
if(condition) {
for(i=0;i<scripts.length;i++) {
script=document.createElement('script');
script.src=scripts[i];
document.getElementsByTagName('head')[0].appendChild(script);
}
}
</script>
This creates an array of the new script directories and names, and loops through them, adding them to the head of the document dynamically.

HTML5 Boilerplate and cirical rendering path / deffering scripts and styles

I used to build my websites based on the HTML5 Boilerplate: styles and modenizr in the head, jQuery (google CDN or hosted file) and scripts before the closing body tag. Something like that:
<!DOCTYPE html>
<!-- modernizr conditional comments here -->
<html class="no-js">
<head>
<link rel="stylesheet" href="css/normalize.css">
<link rel="stylesheet" href="css/main.css">
<script src="js/vendor/modernizr-2.6.2.min.js"></script>
</head>
<body>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="js/vendor/jquery-1.10.2.min.js"><\/script>')</script>
<script src="js/plugins.js"></script>
<script src="js/main.js"></script>
</body>
</html>
Now I want to remove all render-blocking below-the-fold css and js as suggested by Googles PageSpeed Insight.
How do I defer the css and js files including the jQuery library loaded from google?
What should I do about modernizr?
To remove this particular warning you need to do the following:
defer the load of all external CSS until after page onload
defer the load of all external JS until after page onload
In practice that means you need to do the following:
split your CSS into that required to avoid the "flash of unstyled content" (FOUC) and the rest
split your javascript likewise
inline the CSS and JS that is required
defer the load of the other CSS and JS until after page onload.
Using build tools is the only sane way of doing this. You can do it with various Grunt tools, or with the ant-based H5BP Build Script.
The basic method of deferring loads is as follows:
(function () {
// Load jQuery after page onload
function loadJS() {
var url = "https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js";
var n = document.createElement("script");
n.src = url;
n.onload = loadJS1;
document.body.appendChild(n);
}
// Load some JS after jquery has been loaded.
function loadJS1() {
var url = "js/main.js";
var n = document.createElement("script");
n.src = url;
// Continue chaining loads if needed.
//n.onload = loadJS2;
document.body.appendChild(n);
}
// Check for browser support of event handling capability
if (window.addEventListener) {
window.addEventListener("load", loadJS, false);
} else if (window.attachEvent) {
window.attachEvent("onload", loadJS);
} else {
window.onload = loadJS;
}
})();

Categories

Resources