1

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.

user1111929
  • 6,050
  • 9
  • 43
  • 73

2 Answers2

1

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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
tevemadar
  • 12,389
  • 3
  • 21
  • 49
  • Thanks! Writing the fallback code in a .js file sounds like a very clean and nice workaround, but what should that file look like then? As I don't think `document.write()` is still appropriate at that point. – user1111929 Dec 28 '20 at 13:21
  • Prior to inventing anything, I would actually try. If `document.write()` worked so far (here I assume you've tested it via providing non-working link deliberately or something like that), I'm not sure why it wouldn't work now. – tevemadar Dec 28 '20 at 13:28
  • No, you can't use defer and retain the order of the scripts in case of a failure of one of them. If you load A, B, and C, and A fails, B and C will run. A failed `defer` script doesn't prevent `DOMContentLoaded` forever. Re `document.write`: You can't use `document.write` to handle `defer`'d script failure. (You can't use it in an error handler at all, it'll wipe out the page content.) CC @user1111929 – T.J. Crowder Dec 28 '20 at 14:27
1

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.)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Noticed this warning in he Chrome console: `A parser-blocking, cross site (i.e. different eTLD+1) script, https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js, is invoked via document.write. The network request for this script MAY be blocked by the browser in this or a future page load due to poor network connectivity. If blocked in this page load, it will be confirmed in a subsequent console message. See https://www.chromestatus.com/feature/5718547946799104 for more details.` I presume this isn't too bad as they'll still have the local fallback, but can I hide the warning somehow? – user1111929 Jan 27 '21 at 01:37
  • Also, how does this approach compare to the one from https://javascript.info/onload-onerror, i.e. something along the lines of ` – user1111929 Jan 27 '21 at 02:21