109

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:

  1. Why doesn't inline Javascript code get deferred when defer attribute is present on it?

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

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
user31782
  • 7,087
  • 14
  • 68
  • 143
  • 4
    move your inline javascript to an external file and then defer it as well :) – mike510a Dec 30 '16 at 10:53
  • Also you can programmatically inline your external javascript if you dont want to use html5 – mike510a Dec 30 '16 at 10:55
  • @mike510a I cannot put it in an external file for some reason. It is being generated by php and I don't know about php so I cannot change that code. – user31782 Dec 30 '16 at 10:57
  • 2
    You will have to not defer your jQuery library.. it doesnt have to be in the head of the document tho -- you can move it to the end of the body -- as long as it comes before the inlined script and is not deferred it'll be okay. – mike510a Dec 30 '16 at 10:59
  • 2
    Well then get some who knows about php (optimally the one who wrote the code you're using). Or just drop the `defer` everywhere. – Bergi Dec 30 '16 at 11:00
  • or you can inline the entire jQuery library (bad idea) – mike510a Dec 30 '16 at 11:00
  • It is said that defered scripts are loaded after domcontent ready. I am thinking to put my inline js in domcontentloaded event and there recursively check if $ exist after every 200ms and when it exists then prse the code. – user31782 Dec 30 '16 at 11:01
  • Possible duplicate of [load and execute order of scripts](http://stackoverflow.com/questions/8996852/load-and-execute-order-of-scripts) – Matías Fidemraizer Dec 30 '16 at 11:06
  • Personally, I would remove `defer` from the `code.jquery.com` script tag. (And optionally, instead of getting it from an external site, host that script on your own site.) A miniscule delay on the first page of your site that loads on a given browser. After that, it'll be in the browser's cache. There is such a thing as going overboard adding `defer` attribute! Then none of the answers here are needed - the document ready function will execute when expected. – ToolmakerSteve Oct 16 '19 at 12:30

10 Answers10

168

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.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • Thanks for answer. Is it sure that `DOMContentLoaded` event will fire after all the deferred scripts would have been loaded? – user31782 Dec 30 '16 at 11:06
  • 3
    Yes, that is sure. – trincot Dec 30 '16 at 11:07
  • While this is a good solution, it is still render blocking, which probably defeats the purpose of deferring jQuery in the first place. For a truly non-blocking execution, you'd better mimic the 'defer' attribute on inline javascript blocks using [this trick](https://gist.github.com/RonnyO/2391995). – Amin Mozafari Feb 27 '17 at 09:34
  • I have new confusions aroused. I learned that `DOMContentLoaded` is fired before `CSSOM` is ready. MDN says deferred scripts will execute before DOMContentLoaded. So are all deferred scripts executed before CSSOM is ready? Would not that hamper correct application of style changes with javascript? – user31782 Mar 22 '17 at 08:28
  • When in doubt [use `$(window).load`](http://stackoverflow.com/a/4441398/5459839) which will trigger after CSS has loaded. – trincot Mar 22 '17 at 10:03
  • No no I can't wait for all images to be loaded. I only want to understand how things work. I have run some test and it now appears `DOMContentLoaded` waits untill all stylesheetes are loaded but not applied. This is in contradiction with: https://developers.google.com/web/fundamentals/performance/critical-rendering-path/images/analysis-dom-css-js-async.png – user31782 Mar 22 '17 at 13:20
  • 1
    I must say I have never bumped into problems that showed JS was running with the DOM ready, but with the CSSOM not ready. Maybe(!) if JS accesses CSS properties, this might block JS execution until CSSOM is ready, but I have really no idea. This could be a topic for a new question (but check whether it has not been asked before). – trincot Mar 22 '17 at 13:41
  • I have lots of inline `` should I expect errors if I changed all of these scripts to `` – Accountant م Aug 19 '18 at 03:04
  • 1
    There are cases where code will not work any more if you do that. For instance, if code has `document.write`, then it will behave completely differently. – trincot Aug 19 '18 at 07:19
  • Personally, I would remove `defer` from the `code.jquery.com` script tag. And instead of getting it from an external site, host that script on your own site. A miniscule delay on the first page of your site that loads on a given browser. After that, it'll be in the browser's cache. There is such a thing as going overboard adding `defer` attribute! Then this answer isn't needed. OTOH, this answer *is* a perfectly good solution to the problem. – ToolmakerSteve Oct 16 '19 at 12:30
  • This solution has a major disadvantage: functions declared within window.addEventListener cannot be called from the DOM. See this jsfiddle: https://jsfiddle.net/carda/17gbr9dw/5/ I know that calling javascript from html elements (e.g. is very old style js, but I have to maintain a lot of applications that still have this old code built in somewhere. Any ideas to make this still work? – Benedikt Jul 27 '21 at 10:23
  • @Benedikt, I would not call this a "major disadvantage". The major disadvantage here is that the HTML attribute provides a way for a click to call a function *before* all asynchronous libs might have loaded. The design error is *there*. The fiddle is also too simple, as there is no reason to only define `run` when the document has loaded. Anyway,... many ways to make it work. See for example [this adapted fiddle](https://jsfiddle.net/j3vbqe0u/). But better avoid this code style and use `addEventListener` again to bind the click event ([here](https://jsfiddle.net/j3vbqe0u/1/)). – trincot Jul 27 '21 at 10:31
  • @trincot Thank you for the comment. Anyway this would mean I have to go through all the code in the applications, find javascript function statements and change the syntax to the one suggested by you. This would take me days or weeks of work. I hoped I could wrap the existing code into some statement without the need to change all function statements. – Benedikt Jul 28 '21 at 07:33
67

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>
Brook Jordan
  • 1,223
  • 10
  • 18
  • 15
    That is beyond eval; not only are errors hard to find one must also read base64 :) – Wayne May 19 '19 at 16:04
  • 21
    No need for base64 encoding, just use this: `data:text/javascript,alert('hello world')`. Most people don't realise that dataURLs don't inherently need to be encoded as base64. Working example: https://jsbin.com/vazuruxica/edit?html,output I've suggested an edit to your answer :) – joe Sep 23 '20 at 17:40
17

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

Zombo
  • 1
  • 62
  • 391
  • 407
11

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.

Jai
  • 74,255
  • 12
  • 74
  • 103
  • But what to do about inline scripts? And why don't they follow the order in which it is declared? – user31782 Dec 30 '16 at 10:58
  • @user31782 its because the `(jQuery)` part requires that `jQuery` is defined before its accessed -- and deferring a script means to not define anything in that script until after everything else (including inlined code) – mike510a Dec 30 '16 at 11:02
  • That is because of IIFE syntax you have. That runs immediately and it doesn't wait for dom to get loaded. – Jai Dec 30 '16 at 11:02
  • With your last edit it would still throw the error `jQuery is not defined`. – user31782 Dec 30 '16 at 11:12
10

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)
Michal Miky Jankovský
  • 3,089
  • 1
  • 35
  • 36
2

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.

Defer loading of JavaScript

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.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
shinsenter
  • 62
  • 3
  • 3
    This isn't really an answer, but an advertisement. We're talking about decreasing load times and the suggestion is to add another external script? I can't help but feel like that's counterproductive – Andrew Graber May 03 '22 at 00:32
1

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.

Turab
  • 182
  • 1
  • 11
1

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>
zakariamouhid
  • 81
  • 1
  • 4
0

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.

AnyWhichWay
  • 716
  • 8
  • 11
-2

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>
  • I cannot think of a use case where $ (or jQuery, for that matter) is undefined is an issue per se, since the reason you would want it to be defined is that you want to call a jQuery method. – David A. Gray Apr 24 '23 at 01:43