3

I have these scripts that I want to download/execute asynchronously to prevent page render blocking:

<script src="jQuery CDN" defer></script>
<script src="Bootstrap CDN" defer></script>

Bootstrap depends on jQuery, so they have to be executed in this order. That's why I'm using defer, but my question applies for async as well.

How can I asynchronously fall back to my own hosted version of jQuery or Bootstrap, or any CDN-hosted script for that matter?

I have seen this related question on Stackoverflow, but these solutions don't work because:

  1. it relies on document.write, which cannot be used for async/defer scripts; or:
  2. it does not execute the fallback script immediately, causing problems when jQuery couldn't be loaded and other scripts depending on jQuery would load before the jQuery fallback does.

I tried the following, but this also resulted in issue #2 I described above:

<script src="failing external CDN script" defer onerror="
    var script = document.createElement('script');
    script.async = false;
    script.src = 'my own hosted version the script';
    document.body.appendChild(script);
"></script>
Community
  • 1
  • 1
Rudey
  • 4,717
  • 4
  • 42
  • 84
  • 1
    *"I have these scripts that I want to download/execute asynchronously to prevent page render blocking:"* Why not just put the `script` tags at the end of the page, just before the closing `

    ` tag? They don't render-block there, and you can do the fallback without difficulty.

    – T.J. Crowder Feb 18 '17 at 14:43
  • @T.J.Crowder [`defer` allows the browser to start downloading sooner](http://stackoverflow.com/questions/8996852/load-and-execute-order-of-scripts/8996894?noredirect=1#comment71760187_8996894), and [it helps with PageSpeed Insights scoring](http://stackoverflow.com/questions/42293602/scripts-before-body-still-block-page-rendering-according-to-pagespeed-insight/42310154#42310154). – Rudey Feb 18 '17 at 14:49
  • 1
    I'm 99% sure that first link is just plain wrong; when a script is *downloaded* and when it's *executed* need not be linked (other than that the latter can't happen before the former); browsers scan ahead to find things to download. I also find the second claim highly suspect; any official Google documentation saying that? I happpily get 100% from PageInsights without a `defer` in sight (no pun). – T.J. Crowder Feb 18 '17 at 14:51
  • @T.J.Crowder If you run PageSpeed for [Bootstrap's example](https://developers.google.com/speed/pagespeed/insights/?url=http%3A%2F%2Fgetbootstrap.com%2Fexamples%2Fstarter-template%2F) you'll see it complains about a blocking script that's just before the closing `

    ` tag.

    – Rudey Feb 18 '17 at 14:57
  • Along with a host of other problems. Again: A couple of months back I heard that claim, and it took me about 30 minutes to disprove it, getting a 100% score without a `defer` or `async`. Solve the other problems, and a script right before `

    ` doesn't show up as an issue.

    – T.J. Crowder Feb 18 '17 at 14:58
  • It seems PageSpeed Insights is inconsistent because it sometimes complains about scripts before `

    ` while it shouldn't.

    – Rudey Feb 18 '17 at 15:33
  • 1
    I wouldn't be surprised. :-) But also note that it conflates the CSS and JS warning about render-blocking, and it's common practice to put CSS in the header (triggering the warning, but the detail tells you whether it's CSS or JS). But anyway, my point is, fundamentally script-before-end-body-tag isn't a problem. – T.J. Crowder Feb 18 '17 at 15:38

1 Answers1

3

I have these scripts that I want to download/execute asynchronously to prevent page render blocking

The simplest way to do that is just to put them at the end of the page, just before the closing </body> tag. They don't render-block there, and you can do the fallback trivially:

<script src="jQuery CDN"></script>
<script>
if (typeof jQuery === "undefined") {
    // ...add your local version instead...
}
</script>

You've said in a comment that PageSpeed will complain about script tags just before </body> if they don't have defer. With respect, it doesn't (note that the link to code.jquery.com is intentionally broken):

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="favicon.png">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-12">
            Content here, turns green on click
        </div>
    </div>
</div>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
<script src="https://code.jquery.com/jquery-2.2.4.mn.js"></script>
<script>
if (typeof jQuery === "undefined") {
    document.write('<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"><\/script>');
} 
</script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script>
$(".col-md-12").on("click", function() {
    $(this).css("color", "green");
});
</script>
</body>
</html>

That gets 100/100 from PageSpeed (if you host it properly — compression, etc.).

Now, if I had the CSS link at the bottom, I'd also have inline CSS at the top to style the above-the-fold content. (But I probably wouldn't bother, I'd just have the link at the top and accept the hit. I only put it at the bottom of that example because PageSpeed lumps together JS and CSS in its warning on the topic, and I wanted to demonstrate that script just before </body> doesn't trigger that error...)


But if you want to use defer, you'd have to listen for the error event as shown in your question. And of course, that means you have to hold off adding the scripts that are dependent on jQuery until you've had load from the original script or its replacement, since you can't insert into the pipeline. E.g.:

<script src="failing external CDN script" defer onerror="
    var script = document.createElement('script');
    script.async = false;
    script.onload = addDependencies;
    script.src = 'my own hosted version the script';
    document.body.appendChild(script);
" onload="addDependencies()"></script>

...where addDependencies adds script tags for the things depending on the script.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Your second example is similar to my example in my question. The issue I had with this approach is that the dynamically loaded fallback script will not execute immediately. This causes dependencies on jQuery to throw errors because they're loaded before the jQuery fallback itself is loaded. – Rudey Feb 18 '17 at 15:00
  • 1
    @RuudLenders: Yup, you'd have to wait to load them until you got `load` from the original deferred script or the replacement, you can't insert into the pipeline (like you can with non-`defer`/`async` scripts). So...another good reason not to use `defer`. ;-) – T.J. Crowder Feb 18 '17 at 15:03
  • I guess I'll put the scripts before `

    `, then. I'll ignore PageSpeed Insights for now, as it seems to falsely accuse my scripts of render-blocking.

    – Rudey Feb 18 '17 at 15:34
  • @RuudLenders: Weird, I don't think I've had it do that. Anyway, happy coding! – T.J. Crowder Feb 18 '17 at 15:41