Search code examples
javascripthtmlcdnhtml-rendering

How to asynchronously fall back when external CDN script fails? (async/defer)


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>

Solution

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